🧨 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:
lia-by-librechat[bot] 2026-09-26 20:28:47 -04:00 • committed by GitHub
parent 24f950a484
commit a329e01ac7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 67 additions and 2 deletions

View file

@ -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

View file

@ -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';

View file

@ -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>;