diff --git a/librechat.example.yaml b/librechat.example.yaml index c902c4dc7a..2a719c416e 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -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 diff --git a/packages/api/src/middleware/messageFilterPii.spec.ts b/packages/api/src/middleware/messageFilterPii.spec.ts index 9eb27416fc..04bfb5356a 100644 --- a/packages/api/src/middleware/messageFilterPii.spec.ts +++ b/packages/api/src/middleware/messageFilterPii.spec.ts @@ -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); }); diff --git a/packages/api/src/middleware/messageFilterPii.ts b/packages/api/src/middleware/messageFilterPii.ts index 810800cb64..59f5e8b2e4 100644 --- a/packages/api/src/middleware/messageFilterPii.ts +++ b/packages/api/src/middleware/messageFilterPii.ts @@ -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, + ), }, ];