mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🛡️ fix: Validate messageFilter patterns with the RE2 engine at config load
Replace the syntax blacklist (numeric/named backreferences, lookaround) with authoritative validation: config load now compiles each custom pattern with the same linear-time engine the runtime uses, so any RE2-incompatible construct (including control escapes like \cA) is rejected at load with a clear error instead of being silently dropped at request time. The validator is swappable and defaults to native RegExp so browser builds add no engine; the server wires the RE2-backed check at startup via configureMessageFilterRegexValidator in both entry points.
This commit is contained in:
parent
002f0c57f0
commit
67977a8c05
5 changed files with 83 additions and 18 deletions
|
|
@ -24,6 +24,7 @@ const {
|
|||
maybeInjectQueryDevtoolsBootstrap,
|
||||
preAuthTenantMiddleware,
|
||||
configureServerTimeouts,
|
||||
configureMessageFilterRegexValidator,
|
||||
} = require('@librechat/api');
|
||||
const { connectDb, indexSync } = require('~/db');
|
||||
const initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager');
|
||||
|
|
@ -49,6 +50,9 @@ const optionalJwtAuth = require('./middleware/optionalJwtAuth');
|
|||
const noIndex = require('./middleware/noIndex');
|
||||
const routes = require('./routes');
|
||||
|
||||
/** Reject messageFilter PII patterns the RE2 runtime engine cannot compile, at config load. */
|
||||
configureMessageFilterRegexValidator();
|
||||
|
||||
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};
|
||||
|
||||
/** Allow PORT=0 to be used for automatic free port assignment */
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const {
|
|||
configureServerTimeouts,
|
||||
setupGracefulShutdown,
|
||||
updateInterfacePermissions,
|
||||
configureMessageFilterRegexValidator,
|
||||
} = require('@librechat/api');
|
||||
const { connectDb, indexSync } = require('~/db');
|
||||
const {
|
||||
|
|
@ -56,6 +57,9 @@ const staticCache = require('./utils/staticCache');
|
|||
const noIndex = require('./middleware/noIndex');
|
||||
const routes = require('./routes');
|
||||
|
||||
/** Reject messageFilter PII patterns the RE2 runtime engine cannot compile, at config load. */
|
||||
configureMessageFilterRegexValidator();
|
||||
|
||||
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};
|
||||
|
||||
// Allow PORT=0 to be used for automatic free port assignment
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { messageFilterPiiSchema, setMessageFilterRegexValidator } from 'librechat-data-provider';
|
||||
import type { MessageFilterPiiConfig } from 'librechat-data-provider';
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => ({
|
||||
logger: { warn: jest.fn(), error: jest.fn(), info: jest.fn(), debug: jest.fn() },
|
||||
}));
|
||||
import { createMessageFilterPii, findPiiMatchInMessages } from './messageFilterPii';
|
||||
import {
|
||||
createMessageFilterPii,
|
||||
findPiiMatchInMessages,
|
||||
configureMessageFilterRegexValidator,
|
||||
} from './messageFilterPii';
|
||||
|
||||
type CapturedResponse = { status?: number; body?: unknown };
|
||||
|
||||
|
|
@ -366,3 +371,30 @@ describe('findPiiMatchInMessages', () => {
|
|||
expect(hit).toEqual({ id: 'org', label: 'Org token' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('configureMessageFilterRegexValidator (RE2 config-load validation)', () => {
|
||||
afterAll(() => {
|
||||
setMessageFilterRegexValidator((value) => {
|
||||
try {
|
||||
new RegExp(value, 'g');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects RE2-incompatible custom patterns at config parse once wired', () => {
|
||||
configureMessageFilterRegexValidator();
|
||||
const reject = (regex: string) =>
|
||||
messageFilterPiiSchema.safeParse({ customPatterns: [{ id: 'a', label: 'A', regex }] })
|
||||
.success;
|
||||
// lookahead, numeric + named backreference, control escape: all valid JS, unsupported by RE2
|
||||
expect(reject('(?=x)y')).toBe(false);
|
||||
expect(reject('(a)\\1')).toBe(false);
|
||||
expect(reject('(?<n>x)\\k<n>')).toBe(false);
|
||||
expect(reject('token-\\cA+')).toBe(false);
|
||||
// a normal RE2-compatible pattern still passes
|
||||
expect(reject('\\bORG-[A-Z0-9]{6,}')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { RE2JS } from 're2js';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import { setMessageFilterRegexValidator } from 'librechat-data-provider';
|
||||
import type {
|
||||
NextFunction,
|
||||
RequestHandler,
|
||||
|
|
@ -9,6 +10,23 @@ import type {
|
|||
import type { MessageFilterPiiConfig } from 'librechat-data-provider';
|
||||
import { getReferencedQuotes, mergeQuotedText } from '../utils/quotes';
|
||||
|
||||
/**
|
||||
* Wire the messageFilter PII config validator to the linear-time engine (RE2) so a custom
|
||||
* pattern the runtime cannot compile is rejected at config load rather than silently dropped at
|
||||
* request time. Call once at server startup, before config is parsed; browser builds keep the
|
||||
* native default and pull in no engine.
|
||||
*/
|
||||
export function configureMessageFilterRegexValidator(): void {
|
||||
setMessageFilterRegexValidator((pattern) => {
|
||||
try {
|
||||
RE2JS.compile(pattern);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type CompiledPattern = { id: string; label: string; pattern: RE2JS };
|
||||
|
||||
const STARTER_PATTERNS: CompiledPattern[] = [
|
||||
|
|
|
|||
|
|
@ -1874,29 +1874,36 @@ export type SummarizationConfig = z.infer<typeof summarizationConfigSchema>;
|
|||
|
||||
const customEndpointsSchema = z.array(endpointSchema.partial()).optional();
|
||||
|
||||
/**
|
||||
* Validates a messageFilter PII regex at config load. Defaults to native RegExp so browser
|
||||
* builds add no extra engine; the server injects a check backed by the linear-time runtime
|
||||
* engine (RE2) via setMessageFilterRegexValidator, so a pattern the runtime cannot compile
|
||||
* (backreferences, lookaround, control escapes, and so on) is rejected at load rather than
|
||||
* silently dropped at request time.
|
||||
*/
|
||||
let messageFilterRegexValidator: (pattern: string) => boolean = (value) => {
|
||||
try {
|
||||
new RegExp(value, 'g');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const setMessageFilterRegexValidator = (validate: (pattern: string) => boolean): void => {
|
||||
messageFilterRegexValidator = validate;
|
||||
};
|
||||
|
||||
const messageFilterPiiCustomPatternSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
label: z.string().min(1),
|
||||
regex: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(
|
||||
(value) => {
|
||||
// The server runs these patterns through a linear-time engine (RE2), which does not
|
||||
// support backreferences (numeric `\1` or named `\k<name>`) or lookaround. Reject those
|
||||
// at config load with actionable feedback instead of silently dropping them at runtime.
|
||||
if (/\\[1-9]/.test(value) || /\\k[<']/.test(value) || /\(\?<?[=!]/.test(value)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
new RegExp(value, 'g');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ message: 'Unsupported regex: backreferences and lookaround are not supported' },
|
||||
),
|
||||
.refine((value) => messageFilterRegexValidator(value), {
|
||||
message:
|
||||
'Unsupported regex: not compatible with the RE2 engine (no backreferences, lookaround, or control escapes)',
|
||||
}),
|
||||
});
|
||||
|
||||
export const messageFilterPiiSchema = z.object({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue