mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-11-12 09:45:47 +00:00
🧨 fix: Reject Invalid AES Credentials Before Startup (#16402)
* 🧨 fix: Reject Invalid AES Credentials Before Startup * 🎯 test: Format Credential Validation Cases * 🧪 test: Use Valid AES Credentials in API Fixtures --------- Co-authored-by: Lia <lia@librechat.ai>
This commit is contained in:
parent
24f950a484
commit
a329e01ac7
3 changed files with 67 additions and 2 deletions
|
|
@ -29,8 +29,9 @@ process.env.BAN_INTERVAL = '20';
|
|||
process.env.CI = 'true';
|
||||
process.env.JWT_SECRET = 'test';
|
||||
process.env.JWT_REFRESH_SECRET = 'test';
|
||||
process.env.CREDS_KEY = 'test';
|
||||
process.env.CREDS_IV = 'test';
|
||||
// Startup rejects malformed explicitly configured AES credentials. Use valid test-only hex fixtures.
|
||||
process.env.CREDS_KEY = '0123456789abcdef'.repeat(4);
|
||||
process.env.CREDS_IV = '0123456789abcdef'.repeat(2);
|
||||
process.env.ALLOW_EMAIL_LOGIN = 'true';
|
||||
|
||||
// Set global test timeout to 30 seconds
|
||||
|
|
|
|||
|
|
@ -109,6 +109,57 @@ describe('credentials', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
['passphrase', 'not-a-hex-key'],
|
||||
['short AES-128 key', 'a'.repeat(32)],
|
||||
['short AES-192 key', 'a'.repeat(48)],
|
||||
['odd-length key', 'a'.repeat(63)],
|
||||
['trailing non-hex', `${'a'.repeat(64)}!`],
|
||||
['whitespace', ` ${'a'.repeat(64)}`],
|
||||
])('refuses startup for a configured %s without logging the key', (_case, value) => {
|
||||
process.env.CREDS_KEY = value;
|
||||
|
||||
expect(() => bootstrapCredentials()).toThrow(
|
||||
'[credentials] CREDS_KEY must be exactly 64 hexadecimal characters (32 bytes).',
|
||||
);
|
||||
expect(() => bootstrapCredentials()).not.toThrow(value);
|
||||
expect(getCredentialRuntimeState()).toBeUndefined();
|
||||
expect(fs.existsSync(tempFile)).toBe(false);
|
||||
expect(process.env.CREDS_KEY).toBe(value);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['passphrase', 'not-hex'],
|
||||
['short IV', 'b'.repeat(30)],
|
||||
['overlong IV', 'b'.repeat(64)],
|
||||
['trailing non-hex', `${'b'.repeat(32)}!`],
|
||||
])(
|
||||
'refuses startup for a configured %s even when temporary credentials exist',
|
||||
(_case, value) => {
|
||||
fs.writeFileSync(tempFile, `CREDS_IV=${'b'.repeat(32)}\n`);
|
||||
process.env.CREDS_IV = value;
|
||||
|
||||
expect(() => bootstrapCredentials()).toThrow(
|
||||
'[credentials] CREDS_IV must be exactly 32 hexadecimal characters (16 bytes).',
|
||||
);
|
||||
expect(getCredentialRuntimeState()).toBeUndefined();
|
||||
expect(process.env.CREDS_KEY).toBeUndefined();
|
||||
expect(process.env.CREDS_IV).toBe(value);
|
||||
},
|
||||
);
|
||||
|
||||
it('accepts exact-length uppercase hex credentials without replacing them', () => {
|
||||
process.env.CREDS_KEY = 'A'.repeat(64);
|
||||
process.env.CREDS_IV = 'B'.repeat(32);
|
||||
|
||||
const state = bootstrapCredentials();
|
||||
|
||||
expect(state.sources.CREDS_KEY).toBe('environment');
|
||||
expect(state.sources.CREDS_IV).toBe('environment');
|
||||
expect(process.env.CREDS_KEY).toBe('A'.repeat(64));
|
||||
expect(process.env.CREDS_IV).toBe('B'.repeat(32));
|
||||
});
|
||||
|
||||
it('preserves explicitly configured JWT secrets for backward compatibility', () => {
|
||||
process.env.JWT_SECRET = 'short-but-explicit';
|
||||
process.env.JWT_REFRESH_SECRET = 'another-explicit-value';
|
||||
|
|
|
|||
|
|
@ -279,6 +279,19 @@ export function bootstrapCredentials(): CredentialRuntimeState {
|
|||
return existingState;
|
||||
}
|
||||
|
||||
// Validate explicit credentials before generating or adopting any missing values.
|
||||
for (const name of ['CREDS_KEY', 'CREDS_IV'] as const) {
|
||||
const value = process.env[name];
|
||||
if (isConfiguredCredential(value) && !isUsableTemporaryCredential(name, value)) {
|
||||
const bytes = name === 'CREDS_KEY' ? 32 : 16;
|
||||
throw new Error(
|
||||
`[credentials] ${name} must be exactly ${bytes * 2} hexadecimal characters (${bytes} bytes). ` +
|
||||
'Refusing startup to prevent weak encryption or late crypto failures. ' +
|
||||
'Restore the original credential if it protects existing data; plan a controlled migration before rotating it.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const filePath = getCredentialPath();
|
||||
const file = readCredentialFile(filePath);
|
||||
const sources = {} as Record<CredentialName, CredentialSource>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue