From a329e01ac751352792db00db2c156ec6673e35b1 Mon Sep 17 00:00:00 2001 From: "lia-by-librechat[bot]" <328778573+lia-by-librechat[bot]@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:28:47 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=A8=20fix:=20Reject=20Invalid=20AES=20?= =?UTF-8?q?Credentials=20Before=20Startup=20(#16402)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🧨 fix: Reject Invalid AES Credentials Before Startup * 🎯 test: Format Credential Validation Cases * 🧪 test: Use Valid AES Credentials in API Fixtures --------- Co-authored-by: Lia --- api/test/jestSetup.js | 5 +-- packages/api/src/credentials.spec.ts | 51 ++++++++++++++++++++++++++++ packages/api/src/credentials.ts | 13 +++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/api/test/jestSetup.js b/api/test/jestSetup.js index f358027cae..d9d529f75c 100644 --- a/api/test/jestSetup.js +++ b/api/test/jestSetup.js @@ -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 diff --git a/packages/api/src/credentials.spec.ts b/packages/api/src/credentials.spec.ts index 180d57d24b..230047b0f8 100644 --- a/packages/api/src/credentials.spec.ts +++ b/packages/api/src/credentials.spec.ts @@ -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'; diff --git a/packages/api/src/credentials.ts b/packages/api/src/credentials.ts index 2b2f0e261f..a611e0b537 100644 --- a/packages/api/src/credentials.ts +++ b/packages/api/src/credentials.ts @@ -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;