fix(db): escape regex metacharacters in group model patterns - #11311
Merged
diegosouzapw merged 2 commits intoAug 24, 2026
Merged
diegosouzapw merged 2 commits into
diegosouzapw merged 2 commits into
Conversation
`matchesModelPattern()` compiled an operator's group pattern into a RegExp
with only `*` substituted, so every other metacharacter kept its regex
meaning:
"gpt-4.1*" vs "gpt-4o1-preview" -> denied ('.' matched 'o')
"gpt-4(*" vs "gpt-4o" -> SyntaxError: Unterminated group
"claude-3[*" vs "claude-3-opus" -> SyntaxError: Unterminated character class
"*+*" vs "anything" -> SyntaxError: Nothing to repeat
The throw is not contained: `isModelAllowedForKey()` calls the group check
with no try/catch, and it runs on the completion path and on the /v1/models
catalog, so one malformed pattern breaks every request for keys in that
group. The over-match is quieter but worse on an allow rule, which then
grants models the pattern never named.
Escape the metacharacters before substituting `*`, keeping the semantics
this function already had (case-sensitive, `*`-only) and matching how the
rest of the repo compiles operator patterns (`globToRegex`,
`matchesWildcardPattern`).
diegosouzapw
merged commit Aug 24, 2026
6945bba
into
diegosouzapw:release/v3.8.50
14 of 16 checks passed
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…uzapw#11311) Validated on a 17-PR combined board: group-model-pattern-regex-escape within the board's 287/287, typecheck:core clean. matchesModelPattern() only substituted * before compiling to RegExp — every other metacharacter kept its regex meaning, so a malformed group pattern (unbalanced parens/brackets) threw uncaught and broke EVERY request for keys in that group, not just the malformed rule (isModelAllowedForKey has no try/catch and runs on the chat completion path and the /v1/models catalog). Thank you @ntdat812!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
matchesModelPattern()insrc/lib/db/apiKeyGroups.tscompiled an operator's group pattern into a RegExp with only*substituted:Every other metacharacter kept its regex meaning. Measured on
release/v3.8.50(3192eb88d) through the realcheckKeyModelAccess(), with a deny rule and the key in the group:gpt-4.1*gpt-4o1-preview.matchedogpt-4(*gpt-4oSyntaxError: Invalid regular expression: /^gpt-4(.*$/: Unterminated groupclaude-3[*claude-3-opusUnterminated character class*+*anythingNothing to repeatmodelPatternreaches the DB as free text —POST /api/keys/groups/[id]/permissionsvalidates it asz.string().trim().min(1)and nothing narrows it afterwards.Why the throw matters
It is not contained.
isModelAllowedForKey()(src/lib/db/apiKeys.ts:1563) calls the group check with no try/catch, and that helper runs on the completion path (src/sse/handlers/chat.ts:942) and on the/v1/modelscatalog (src/app/api/v1/models/catalogResponse.ts:209). Confirmed end to end:So one malformed pattern breaks every request for keys in that group, not just the rule that carries it.
The over-match is quieter but worse in one direction: on a deny rule it blocks unrelated models, on an allow rule it grants models the operator never named.
The fix
Escape the metacharacters before substituting
*. This keeps the semantics the function already had — case-sensitive,*-only, no?wildcard — so no existing pattern changes meaning except the ones that were being read as regexes. It also brings this call site in line with how the rest of the repo compiles operator patterns (globToRegexinsrc/shared/utils/globPattern.ts,matchesWildcardPatterninsrc/lib/db/apiKeys/modelPermissions.ts); this was the last unescaped one.I deliberately did not switch to
globToRegex: it is case-insensitive and treats?as a wildcard, which would silently widen existing allow rules.Tests
tests/unit/group-model-pattern-regex-escape.test.ts— 7 tests through the realcheckKeyModelAccess()andisModelAllowedForKey(), no mocks.They are load-bearing, not decorative. Removing the escape (keeping the rest of the fix) fails 5 of the 7:
The two that still pass are the ones pinning unchanged behaviour, which is what they are for.
Verification
db-api-key-groups,group-provider-permission,model-catalog-policy-invalidation-8728,api-key-policy,api-key-scope-validation,api-key-lifecycle): 83 tests, 82 pass. The one failure isapi-key-lifecycle—EBUSY: resource busy or locked, unlink '…\storage.sqlite'during teardown on Windows; all 11 of its assertions pass. It fails identically withsrc/lib/db/apiKeyGroups.tsrestored fromHEAD, so it is pre-existing and environment-specific, not this change.npm run typecheck:core: clean.eslinton both files: clean.prettier --checkreports the same pre-existing formatting drift onapiKeyGroups.tsbefore and after this change (the file uses trailing commas the config would strip), so I left the rest of the file alone rather than shipping a whole-file reformat.Found by audit while looking for unescaped dynamic
RegExpconstruction, not from a reported incident — no user report is attached to it.No new ESLint warningsfails on this PR withThat is a stale entry in
config/quality/eslint-suppressions.json, not a warning introduced here:config/quality/eslint-suppressions.jsonhas no entry forsrc/lib/db/apiKeyGroups.ts,eslinton both changed files is clean locally, and the same job is red on the other open PRs against this base (#11307, #11308, #11309 — including the maintainer's own). Left alone rather than pruned from a contributor branch.