Skip to content

fix: keep # and ; inside quoted model values from being read as comments - #1770

Closed
NUAAwqy wants to merge 1 commit into
apache:masterfrom
NUAAwqy:fix/matcher-comment-in-quoted-value
Closed

NUAAwqy wants to merge 1 commit into
apache:masterfrom
NUAAwqy:fix/matcher-comment-in-quoted-value

Conversation

@NUAAwqy

@NUAAwqy NUAAwqy commented Nov 2, 2026

Copy link
Copy Markdown

What

A # or ; inside a quoted value of a model .conf file is treated as the start of a comment, so the value is silently truncated and enforcement fails.

[matchers]
m = r.sub == p.sub && r.act == "#write"    # only writing is allowed

The value #write contains a comment marker, so the matcher is read as:

r.sub == p.sub && r.act == "

and Enforce returns unclosed string literal instead of a decision. A matcher literal that legitimately contains # or ; (a URL fragment, a tag, a role name) cannot be written in a model file at all today.

Root cause

Two places cut the line at the first #/; without knowing about quoted literals:

  1. config/config.go — parseBuffer() scans the raw line for DEFAULT_COMMENT / DEFAULT_COMMENT_SEM and writes only the prefix before write() ever splits option = value, so the truncation happens while the file is being read.
  2. util/util.go — RemoveComments() used strings.Index(s, "#"). It is applied to the matcher in model/model.go, enforcer.go and management_api.go, so even a value that survives parsing is truncated again on the way to the expression engine.

EscapeAssertion() already skips quoted literals when rewriting r./p. prefixes (added in 9a5c253), so the two comment scanners were the remaining inconsistent case.

Fix

Skip a terminated string literal (with \ escaping) when looking for the comment marker, in both scanners:

  • config/config.go: the inline byte scan is replaced by optionValueEnd(), which returns the first #/; outside a quoted literal, or the end of the line.
  • util/util.go: RemoveComments() walks the string the same way. The literal scanner that EscapeAssertion() already needed is factored out into stringLiteralEnd() and reused; EscapeAssertion() behavior is unchanged.

An unterminated literal is deliberately left alone: m = r.act == "#write still becomes r.act == ", so the expression parser reports the missing quote rather than the config parser hiding it behind a shortened value.

Tests

Added regression tests, all of which fail on current master and pass with the fix:

  • TestCommentMarkerInsideQuotedValue (config/config_test.go): #, single-quoted #, escaped-quote #, and the "value + real comment" combinations through the actual file/text parser.
  • TestRemoveComments (util/util_test.go): the same cases at the util level, plus # it's a comment (an apostrophe in a trailing comment must not swallow the rest of the line) and the unterminated-literal case.
  • TestMatcherWithStringLiteralContainingHash (enforcer_test.go): end-to-end NewModelFromString + Enforce, asserting #write is allowed and read is denied.

Verification performed locally against master (d3b2659):

  • Before the fix: the three new tests fail (config values truncated to r.act == ", and Enforce Error: unclosed string literal).
  • After the fix: go test ./... passes for the whole module, including the existing TestGet/TestEscapeAssertion/TestRemoveComments cases, and gofmt -l is clean.

Notes

The config helper is a []byte variant duplicated from the util one rather than shared: config currently depends only on the standard library, and reaching into util (which pulls in rbac) to reuse a 12-line scanner over a different type seemed like the worse trade. I'm happy to switch to a shared exported helper if you'd prefer that.

This PR is deliberately limited to the comment/quoted-value interaction; no other parser behavior changes.

Disclosure: I used an AI-assisted agent to help locate the two truncation sites, draft the fix, and write the tests; the reproduction and the verification results above are my own, and the code is what I would submit and maintain.

The mini-ini parser in config/config.go and util.RemoveComments both treat
the first '#' or ';' as the start of a comment, so a comment marker inside a
quoted value silently truncates the value. A matcher such as
m = r.sub == p.sub && r.act == "#write" then reaches the expression engine as
r.act == " and enforcement fails with "unclosed string literal".

Skip terminated string literals when looking for a comment marker in both
places. An unterminated literal is left untouched so that the missing quote,
rather than a silently shortened matcher, is what gets reported.

Co-authored-by: AI assistant (analysis and code drafted with an AI agent)
@hsluoyz

hsluoyz commented Nov 2, 2026

Copy link
Copy Markdown
Member

Thanks for the report and the clear write-up, the bug is real. It's fixed in 6a018f2 with a slightly different approach, so I'm closing this PR.

The difference is how a backslash inside a quoted string is treated. In matchers it is not an escape character: EscapeStringLiterals doubles every backslash so that matchers behave like CSV policies, which means "a\" is the string a\ and \" can never keep a literal open. This PR (and the existing EscapeAssertion) treated \" as an escaped quote, so r.act == "a\"#w" went from a truncated but valid matcher to Invalid token: '#'. The commit uses one rule everywhere, a literal ends at the next matching quote, which also lets the scanner shrink to a single IndexByte and drops the special case for unterminated literals (the expression parser already reports those).

@hsluoyz hsluoyz closed this Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants