Skip to content

[bug] Fix case-sensitive matching of SDDL keyword operators in conditional expressions (Fixes #131) - #132

Merged
p0dalirius merged 1 commit into
mainfrom
bugfix-condition-operator-case-insensitive
Jul 29, 2026
Merged

[bug] Fix case-sensitive matching of SDDL keyword operators in conditional expressions (Fixes #131)#132
p0dalirius merged 1 commit into
mainfrom
bugfix-condition-operator-case-insensitive

Conversation

@p0dalirius

Copy link
Copy Markdown
Collaborator

Linked Issue

Closes #131

Root Cause

Keyword operators were resolved by indexing wordOperators with the raw lexed token text (parser.go:220), and by a literal switch on that same text for the infix operators (parser.go:285296). The lexer preserves the original case of a tkWord token — correctly, since attribute names are case-preserving — so each operator parsed under exactly one capitalisation, the one hardcoded in the map and the switch.

That is not merely a strictness issue, because two capitalisations of the same four tokens are in circulation. MS-DTYP 2.4.4.17.6 spells them Member_of_Any, and wordOperators followed the specification. The operator-name tables in Windows' own sechost.dll and advapi32.dll spell them Member_of_any, with a lowercase any. The parser therefore rejected conditions carrying the spelling Windows itself produces. The failure was also badly reported: an unrecognised word falls through to being parsed as an attribute name, so the error surfaced as expected ')' to close parenthesized expression and never named the operator.

Fix Description

Keyword-operator lookup now goes through lookupWordOperator, which matches against wordOperatorsFolded — a lowercase-keyed copy of wordOperators built once at package initialisation. Both call sites use it.

wordOperators is kept as the canonical, specification-cased table and remains the single source of truth; the folded map is derived from it, so adding an operator in one place keeps both directions consistent. Lowercasing the lookup key was chosen over lowercasing in the lexer because the lexer's output doubles as attribute names, which must retain their case.

The infix site needs to admit only the operators that take a left- and a right-hand side. Replacing its switch with the shared lookup would have wrongly accepted the unary Member_of family and Exists/Not_Exists in infix position, so the lookup is gated on a new isRelationalWordOperator predicate, mirroring the existing isMemberOfOperator helper. Behaviour for unary operators in infix position is unchanged, and there is a test pinning that.

Serialization is untouched: operatorNames is keyed by token byte and keeps emitting the MS-DTYP capitalisation, so output remains stable and specification-conformant regardless of the case that was parsed.

How Verified

Runtime, before the fixace/condition.Parse on each casing:

Member_of_Any  (MS-DTYP spelling)              accepted
Member_of_any  (Windows' spelling)             REJECTED: expected ')' to close parenthesized expression
member_of                                      REJECTED: expected ')' to close parenthesized expression
MEMBER_OF                                      REJECTED: expected ')' to close parenthesized expression
exists                                         REJECTED: expected ')' to close parenthesized expression
contains                                       REJECTED: expected ')' to close parenthesized expression
ANY_OF                                         REJECTED: expected ')' to close parenthesized expression

Runtime, after the fix — all of the above parse, and each variant marshals to bytes identical to the canonical spelling.

Regression check — with the new tests in place but condition.go/parser.go reverted to their pre-fix state, go test ./ace/condition/... reports 27 failures. With the fix applied the package passes, and go test ./... is green across the repository. gofmt -l ace/ is clean and go vet ./ace/... reports nothing.

Test Coverage

Added — in ace/condition/condition_test.go:

  • TestKeywordOperatorsAreCaseInsensitive — every keyword operator across mixed, lower and upper casings, asserting each variant marshals to the same bytes as the canonical spelling. Covers both parse paths and both spellings of the four any operators.
  • TestSerializeUsesSpecCapitalization — parses Member_of_any and asserts the re-serialized text contains Member_of_Any, pinning that the fix did not leak into the output form.
  • TestUnaryKeywordOperatorsRejectedInInfixPosition — asserts Member_of and Exists are still rejected between a left- and right-hand side, in both casings, guarding the isRelationalWordOperator gate.

Scope of Change

  • Files changed: ace/condition/condition.go, ace/condition/parser.go, ace/condition/condition_test.go
  • Submodule pointer updated: no
  • Behavioral changes outside the bug fix: none. One consequence internal to the fix is worth flagging for review: keyword operators are now reserved in every casing, so an expression using a keyword as a local attribute name in non-canonical case — (member_of == 1), say — is now an error where it previously parsed as an attribute. The canonical casing was already reserved this way before the change, so this makes the existing behaviour consistent across casings rather than introducing a new rule.

Risk and Rollout

Confined to the conditional-expression parser; the encoder, decoder and serializer are untouched. The change strictly widens the set of accepted inputs, apart from the keyword-shadowing consequence noted above. Safe to merge without staged rollout.

Notes

The Member_of_any spelling was found by recovering the 24-entry operator table from sechost.dll (.rdata RVA 0x070300) and advapi32.dll (RVA 0x06f1e0) on Windows Server 2025; the two tables are byte-identical and both use the lowercase any form.

Two further divergences between those tables and MS-DTYP were observed and are deliberately not addressed here, to keep this change focused. Each warrants its own issue:

  • Token 0xa3, named & with precedence 10, is present in both Windows tables but is not defined anywhere in MS-DTYP. This package has no constant for it, so such an ACE cannot round-trip.
  • @TOKEN. appears as a fourth attribute prefix alongside the documented @USER., @DEVICE. and @RESOURCE., and is currently rejected as an unknown prefix.

…expressions (Fixes #131)

Keyword operators were resolved by indexing wordOperators with the raw lexed
token text, and by a literal switch on that text for the infix operators. The
lexer preserves the case of a word token, correctly, because attribute names are
case-preserving, so only one exact capitalisation of each operator parsed.

That rejected the spelling Windows itself uses: the operator-name tables in
sechost.dll and advapi32.dll spell the four "any" operators with a lowercase
any (Member_of_any), whereas MS-DTYP 2.4.4.17.6 and wordOperators spell them
Member_of_Any. Because an unrecognised word falls through to being parsed as an
attribute name, the resulting error did not even name the operator.

Resolve keyword operators through a new lookupWordOperator helper backed by a
case-folded copy of wordOperators, at both call sites. The infix site keeps its
restriction to the four binary keyword operators via a new
isRelationalWordOperator predicate, so the unary Member_of family and
Exists/Not_Exists are still rejected between a left- and right-hand side.
Serialization is untouched and continues to emit the MS-DTYP capitalisation.
@github-actions github-actions Bot changed the title Fix case-sensitive matching of SDDL keyword operators in conditional expressions (Fixes #131) [bug] Fix case-sensitive matching of SDDL keyword operators in conditional expressions (Fixes #131) Jul 29, 2026
@github-actions github-actions Bot added the bug Something isn't working label Jul 29, 2026
@p0dalirius
p0dalirius merged commit 788126e into main Jul 29, 2026
5 checks passed
@p0dalirius
p0dalirius deleted the bugfix-condition-operator-case-insensitive branch July 29, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant