mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix: fail closed when messageFilter.pii compiles to zero patterns
DB and admin config overrides bypass the RE2 schema validation (it only runs at YAML load), so an override whose only pattern is RE2-incompatible was dropped at compile time, left zero patterns, and let the request through. compile() now returns a failClosed flag when a config declared patterns but every one failed to compile; the middleware returns 400 and findPiiMatchInMessages returns a distinct misconfigured match that the OpenAI and Responses controllers surface with an admin-facing message.
This commit is contained in:
parent
3ae8dbef28
commit
312c81f79b
5 changed files with 63 additions and 7 deletions
|
|
@ -189,7 +189,9 @@ const executeOpenAIChatCompletion = async (envelope, { req, res }) => {
|
|||
return sendErrorResponse(
|
||||
res,
|
||||
400,
|
||||
`Message contains a ${piiHit.label}. Remove it and try again.`,
|
||||
piiHit.misconfigured
|
||||
? 'Message filtering is misconfigured; contact your administrator.'
|
||||
: `Message contains a ${piiHit.label}. Remove it and try again.`,
|
||||
'invalid_request_error',
|
||||
'message_filter_pii_block',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -622,7 +622,9 @@ const executeResponse = async (envelope, { req, res }) => {
|
|||
return sendResponsesErrorResponse(
|
||||
res,
|
||||
400,
|
||||
`Message contains a ${piiHit.label}. Remove it and try again.`,
|
||||
piiHit.misconfigured
|
||||
? 'Message filtering is misconfigured; contact your administrator.'
|
||||
: `Message contains a ${piiHit.label}. Remove it and try again.`,
|
||||
'invalid_request',
|
||||
'message_filter_pii_block',
|
||||
);
|
||||
|
|
|
|||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -137,6 +137,7 @@
|
|||
"pdfjs-dist": "^5.4.624",
|
||||
"prom-client": "^15.1.3",
|
||||
"rate-limit-redis": "^4.2.0",
|
||||
"re2js": "^2.8.6",
|
||||
"sanitize-html": "^2.17.6",
|
||||
"sharp": "^0.35.3",
|
||||
"ua-parser-js": "^1.0.36",
|
||||
|
|
@ -36478,6 +36479,15 @@
|
|||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/re2js": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/re2js/-/re2js-2.8.6.tgz",
|
||||
"integrity": "sha512-xLgQil4kIUCrAzVk9fRSkxkFNwmygLFjVxXrLc65aE1F0+Zsb8rxumFBy4XKyvgMCTL6kilDq3EZ0piE2dP/Dg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
|
|
@ -42703,6 +42713,7 @@
|
|||
"pdfjs-dist": "^5.4.624",
|
||||
"prom-client": "^15.1.3",
|
||||
"rate-limit-redis": "^4.2.0",
|
||||
"re2js": "^2.8.6",
|
||||
"sanitize-html": "^2.17.6",
|
||||
"sharp": "^0.35.3",
|
||||
"undici": "^7.24.1",
|
||||
|
|
|
|||
|
|
@ -403,4 +403,25 @@ describe('configureMessageFilterRegexValidator (RE2 config-load validation)', ()
|
|||
// a normal RE2-compatible pattern still passes
|
||||
expect(reject('\\bORG-[A-Z0-9]{6,}')).toBe(true);
|
||||
});
|
||||
|
||||
it('fails closed with 400 when every configured pattern fails to compile', () => {
|
||||
const { capturedRes, nextCalls } = runMiddleware(
|
||||
{
|
||||
starterPatterns: [],
|
||||
customPatterns: [{ id: 'backref', label: 'Backref', regex: '(a)\\1' }],
|
||||
},
|
||||
{ text: 'anything at all' },
|
||||
);
|
||||
expect(nextCalls).toBe(0);
|
||||
expect(capturedRes.status).toBe(400);
|
||||
expect(capturedRes.body).toMatchObject({ error: 'message_filter_pii_block' });
|
||||
});
|
||||
|
||||
it('findPiiMatchInMessages returns a misconfigured match when every pattern fails to compile', () => {
|
||||
const hit = findPiiMatchInMessages([{ role: 'user', content: 'hello' }], {
|
||||
starterPatterns: [],
|
||||
customPatterns: [{ id: 'backref', label: 'Backref', regex: '(a)\\1' }],
|
||||
});
|
||||
expect(hit?.misconfigured).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -64,25 +64,33 @@ function selectStarter(ids?: string[]): CompiledPattern[] {
|
|||
return out;
|
||||
}
|
||||
|
||||
const COMPILE_CACHE = new WeakMap<object, CompiledPattern[]>();
|
||||
type CompiledConfig = { patterns: CompiledPattern[]; failClosed: boolean };
|
||||
|
||||
function compile(config: MessageFilterPiiConfig): CompiledPattern[] {
|
||||
const COMPILE_CACHE = new WeakMap<object, CompiledConfig>();
|
||||
|
||||
function compile(config: MessageFilterPiiConfig): CompiledConfig {
|
||||
const cached = COMPILE_CACHE.get(config);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
const starter = selectStarter(config.starterPatterns);
|
||||
const custom: CompiledPattern[] = [];
|
||||
let dropped = 0;
|
||||
for (const p of config.customPatterns ?? []) {
|
||||
try {
|
||||
custom.push({ id: p.id, label: p.label, pattern: RE2JS.compile(p.regex) });
|
||||
} catch (err) {
|
||||
dropped += 1;
|
||||
logger.warn(
|
||||
`[messageFilter.pii] dropping invalid or unsupported customPattern ${JSON.stringify(p.id)}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const result = [...starter, ...custom];
|
||||
const patterns = [...starter, ...custom];
|
||||
// Fail closed when a config declared patterns but every one failed to compile (e.g. a DB or
|
||||
// admin override carrying RE2-incompatible syntax that never hit load-time validation): with
|
||||
// nothing left to enforce, block rather than pass.
|
||||
const result: CompiledConfig = { patterns, failClosed: patterns.length === 0 && dropped > 0 };
|
||||
COMPILE_CACHE.set(config, result);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -99,6 +107,8 @@ function findMatch(text: string, patterns: CompiledPattern[]): CompiledPattern |
|
|||
export interface PiiMatch {
|
||||
id: string;
|
||||
label: string;
|
||||
/** Set when the filter is configured but every pattern failed to compile; block without a real matched label. */
|
||||
misconfigured?: boolean;
|
||||
}
|
||||
|
||||
type ContentPart = { type?: string; text?: string; [key: string]: unknown };
|
||||
|
|
@ -114,7 +124,10 @@ export function findPiiMatchInMessages(
|
|||
if (config == null || !Array.isArray(messages) || messages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const patterns = compile(config);
|
||||
const { patterns, failClosed } = compile(config);
|
||||
if (failClosed) {
|
||||
return { id: '__misconfigured__', label: 'restricted value', misconfigured: true };
|
||||
}
|
||||
if (patterns.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -206,7 +219,14 @@ export function createMessageFilterPii(options: CreateMessageFilterPiiOptions):
|
|||
next();
|
||||
return;
|
||||
}
|
||||
const patterns = compile(config);
|
||||
const { patterns, failClosed } = compile(config);
|
||||
if (failClosed) {
|
||||
res.status(400).json({
|
||||
error: 'message_filter_pii_block',
|
||||
message: 'Message filtering is misconfigured; contact your administrator.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (patterns.length === 0) {
|
||||
next();
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue