🛡️ fix: Match the full whitespace set in messageFilter starter patterns

RE2's `\s` omits the vertical tab and `\p{Zs}` omits U+2028, U+2029, and
U+FEFF, so a separator built from one of those characters slipped past the
`api-key` and `Bearer` starter patterns and reached the model. Broaden the
starter whitespace class to the full JavaScript whitespace set so those
separators are covered again.
This commit is contained in:
Dustin Healy 2026-07-31 14:33:58 -07:00
parent 67977a8c05
commit 3ae8dbef28
3 changed files with 22 additions and 8 deletions

View file

@ -894,9 +894,12 @@ endpoints:
# # (optional) Pick a subset of the starter catalog by id; omit to
# # enable all starters (sk_prefix, bearer_header, api_key_header).
# starterPatterns: [sk_prefix, bearer_header, api_key_header]
# # (optional) Operator-defined patterns. Each entry needs id,
# # label, and a regex in RE2 syntax (backreferences and lookaround
# # are not supported); the regex is validated at config load time.
# # (optional) Operator-defined patterns. Each entry needs id, label,
# # and a regex in RE2 syntax and semantics (RE2 is a linear-time engine
# # with no catastrophic backtracking; a few escapes such as \p, \A, and
# # \s differ from JavaScript). Backreferences and lookaround are not
# # supported; the regex is validated against the RE2 engine at config
# # load time and a pattern it cannot compile is rejected.
# customPatterns:
# - id: anthropic_api_key
# label: Anthropic API key

View file

@ -94,9 +94,15 @@ describe('messageFilterPii middleware', () => {
expect(capturedRes.status).toBe(400);
});
it('rejects an api-key header separated by non-ASCII whitespace', () => {
const nbsp = String.fromCharCode(0x00a0);
const { capturedRes, nextCalls } = runMiddleware({}, { text: `api-key:${nbsp}foo123bar` });
it.each([
['U+00A0 no-break space', 0x00a0],
['U+000B vertical tab', 0x000b],
['U+2028 line separator', 0x2028],
['U+2029 paragraph separator', 0x2029],
['U+FEFF zero-width no-break space', 0xfeff],
])('rejects an api-key header separated by %s', (_label, code) => {
const ws = String.fromCharCode(code);
const { capturedRes, nextCalls } = runMiddleware({}, { text: `api-key:${ws}foo123bar` });
expect(nextCalls).toBe(0);
expect(capturedRes.status).toBe(400);
});

View file

@ -29,17 +29,22 @@ export function configureMessageFilterRegexValidator(): void {
type CompiledPattern = { id: string; label: string; pattern: RE2JS };
const WHITESPACE = '\\s\\p{Zs}\\x0B\\x{2028}\\x{2029}\\x{FEFF}';
const STARTER_PATTERNS: CompiledPattern[] = [
{ id: 'sk_prefix', label: 'sk- prefix token', pattern: RE2JS.compile('\\b(sk-)[a-zA-Z0-9_-]+') },
{
id: 'bearer_header',
label: 'Bearer token',
pattern: RE2JS.compile('\\b(Bearer )[^\\s\\p{Zs}"\']+', RE2JS.CASE_INSENSITIVE),
pattern: RE2JS.compile(`\\b(Bearer )[^${WHITESPACE}"']+`, RE2JS.CASE_INSENSITIVE),
},
{
id: 'api_key_header',
label: 'api-key header',
pattern: RE2JS.compile('\\b(api-key:?[\\s\\p{Zs}]+)[^\\s\\p{Zs}"\']+', RE2JS.CASE_INSENSITIVE),
pattern: RE2JS.compile(
`\\b(api-key:?[${WHITESPACE}]+)[^${WHITESPACE}"']+`,
RE2JS.CASE_INSENSITIVE,
),
},
];