mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🔐 fix: Redact Sensitive Tokens Embedded in JSON Metadata
Two gaps in the existing console redaction that became user-visible once
warn/error lines started emitting structured metadata:
1. The OpenAI-key regex (`/^(sk-)[^\s]+/`) was anchored to start-of-line,
so keys embedded inside JSON payloads (e.g. `{"apiKey":"sk-..."}`)
were never redacted. Every console line begins with a timestamp, so
the anchor effectively made this pattern dead code.
2. `formatConsoleMeta` stringified metadata values verbatim; a sensitive
string value was only redacted by the whole-line regex pass, which
missed the anchored `sk-` case above.
Fix:
- Drop the `^` anchor; add `/g` so every occurrence is redacted, not just
the first.
- Also exclude `"` and `'` from the token body so JSON-embedded values
terminate at the closing quote rather than chewing into the next field.
- Simplify `redactMessage` to apply patterns directly (dropping the
`getMatchingSensitivePatterns` filter) — the filter used `.test()`
which has stateful behavior on `/g` regexes and is no longer needed.
- `formatConsoleMeta` now runs `redactMessage` over every string value
before JSON serialization, so the metadata trailer is safe even on the
warn path.
- Add regression tests covering both fixes.
Reviewed-by: Codex (P1 finding on PR #12737, commit 68c31b6).
This commit is contained in:
parent
68c31b6980
commit
e288f7fda7
2 changed files with 72 additions and 30 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
const { formatConsoleMeta } = jest.requireActual('../parsers');
|
const { formatConsoleMeta, redactMessage } = jest.requireActual('../parsers');
|
||||||
|
|
||||||
describe('formatConsoleMeta', () => {
|
describe('formatConsoleMeta', () => {
|
||||||
it('returns empty string when there is no user metadata', () => {
|
it('returns empty string when there is no user metadata', () => {
|
||||||
|
|
@ -80,4 +80,57 @@ describe('formatConsoleMeta', () => {
|
||||||
|
|
||||||
expect(meta).toBe('');
|
expect(meta).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('redacts sensitive patterns inside string metadata values', () => {
|
||||||
|
const meta = formatConsoleMeta({
|
||||||
|
level: 'error',
|
||||||
|
message: 'leak test',
|
||||||
|
timestamp: 'ts',
|
||||||
|
openaiKey: 'sk-abc123def456',
|
||||||
|
auth: 'Bearer eyJhbGciOi...tokenvalue',
|
||||||
|
google: 'https://example.com/?key=AIzaSyXX',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(meta).not.toContain('sk-abc123def456');
|
||||||
|
expect(meta).not.toContain('eyJhbGciOi...tokenvalue');
|
||||||
|
expect(meta).not.toContain('AIzaSyXX');
|
||||||
|
expect(meta).toContain('sk-[REDACTED]');
|
||||||
|
expect(meta).toContain('Bearer [REDACTED]');
|
||||||
|
expect(meta).toContain('key=[REDACTED]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redacts multiple occurrences of the same pattern in one value', () => {
|
||||||
|
const meta = formatConsoleMeta({
|
||||||
|
level: 'error',
|
||||||
|
message: 'two keys',
|
||||||
|
timestamp: 'ts',
|
||||||
|
combined: 'first sk-aaa and then sk-bbb',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(meta).not.toContain('sk-aaa');
|
||||||
|
expect(meta).not.toContain('sk-bbb');
|
||||||
|
expect(meta.match(/sk-\[REDACTED\]/g)?.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('redactMessage', () => {
|
||||||
|
it('redacts sk- keys that are not at line start (inside JSON-like text)', () => {
|
||||||
|
const input = '{"apiKey":"sk-abc123"}';
|
||||||
|
expect(redactMessage(input)).toBe('{"apiKey":"sk-[REDACTED]"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redacts all sk- occurrences in a single pass', () => {
|
||||||
|
const input = 'sk-one sk-two sk-three';
|
||||||
|
expect(redactMessage(input)).toBe('sk-[REDACTED] sk-[REDACTED] sk-[REDACTED]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims redacted output when trimLength is provided', () => {
|
||||||
|
const input = 'Bearer supersecretvalue';
|
||||||
|
expect(redactMessage(input, 10)).toBe('Bearer [RE...');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for falsy input', () => {
|
||||||
|
expect(redactMessage('')).toBe('');
|
||||||
|
expect(redactMessage(undefined)).toBe('');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,26 +8,12 @@ const CONSOLE_JSON_STRING_LENGTH = parseInt(process.env.CONSOLE_JSON_STRING_LENG
|
||||||
const DEBUG_MESSAGE_LENGTH = parseInt(process.env.DEBUG_MESSAGE_LENGTH) || 150;
|
const DEBUG_MESSAGE_LENGTH = parseInt(process.env.DEBUG_MESSAGE_LENGTH) || 150;
|
||||||
|
|
||||||
const sensitiveKeys = [
|
const sensitiveKeys = [
|
||||||
/^(sk-)[^\s]+/, // OpenAI API key pattern
|
/(sk-)[^\s"']+/g, // OpenAI API key pattern (also catches keys embedded in JSON/quoted strings)
|
||||||
/(Bearer )[^\s]+/, // Header: Bearer token pattern
|
/(Bearer )[^\s"']+/g, // Header: Bearer token pattern
|
||||||
/(api-key:? )[^\s]+/, // Header: API key pattern
|
/(api-key:? )[^\s"']+/g, // Header: API key pattern
|
||||||
/(key=)[^\s]+/, // URL query param: sensitive key pattern (Google)
|
/(key=)[^\s"']+/g, // URL query param: sensitive key pattern (Google)
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if a given value string is sensitive and returns matching regex patterns.
|
|
||||||
*
|
|
||||||
* @param {string} valueStr - The value string to check.
|
|
||||||
* @returns {Array<RegExp>} An array of regex patterns that match the value string.
|
|
||||||
*/
|
|
||||||
function getMatchingSensitivePatterns(valueStr) {
|
|
||||||
if (valueStr) {
|
|
||||||
// Filter and return all regex patterns that match the value string
|
|
||||||
return sensitiveKeys.filter((regex) => regex.test(valueStr));
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redacts sensitive information from a console message and trims it to a specified length if provided.
|
* Redacts sensitive information from a console message and trims it to a specified length if provided.
|
||||||
* @param {string} str - The console message to be redacted.
|
* @param {string} str - The console message to be redacted.
|
||||||
|
|
@ -39,16 +25,16 @@ function redactMessage(str, trimLength) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const patterns = getMatchingSensitivePatterns(str);
|
let redacted = str;
|
||||||
patterns.forEach((pattern) => {
|
for (const pattern of sensitiveKeys) {
|
||||||
str = str.replace(pattern, '$1[REDACTED]');
|
redacted = redacted.replace(pattern, '$1[REDACTED]');
|
||||||
});
|
|
||||||
|
|
||||||
if (trimLength !== undefined && str.length > trimLength) {
|
|
||||||
return `${str.substring(0, trimLength)}...`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return str;
|
if (trimLength !== undefined && redacted.length > trimLength) {
|
||||||
|
return `${redacted.substring(0, trimLength)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redacted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -141,10 +127,13 @@ function formatConsoleMeta(info) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(meta, (_key, value) => {
|
return JSON.stringify(meta, (_key, value) => {
|
||||||
if (typeof value === 'string' && value.length > CONSOLE_JSON_STRING_LENGTH) {
|
if (typeof value !== 'string') {
|
||||||
return `${value.substring(0, CONSOLE_JSON_STRING_LENGTH)}...`;
|
return value;
|
||||||
}
|
}
|
||||||
return value;
|
const safe = redactMessage(value);
|
||||||
|
return safe.length > CONSOLE_JSON_STRING_LENGTH
|
||||||
|
? `${safe.substring(0, CONSOLE_JSON_STRING_LENGTH)}...`
|
||||||
|
: safe;
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue