🥫 fix: Refuse to Boot on Retired JWT Secret Defaults (#15282)

* 🛡️ fix: Reject Retired JWT Defaults

* 🛡️ fix: Reject Legacy Temporary JWT Secrets
This commit is contained in:
Danny Avila 2026-08-27 07:02:05 -04:00 committed by GitHub
parent 8b1fcc0fc2
commit 9423ad47cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 2 deletions

View file

@ -122,6 +122,46 @@ describe('credentials', () => {
expect(state.generated).toEqual(['CREDS_KEY', 'CREDS_IV']);
});
it.each([
['JWT_SECRET', '16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef'],
['JWT_REFRESH_SECRET', 'eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8418'],
] as const)('rejects retired default %s values', (name, value) => {
process.env[name] = value;
expect(() => bootstrapCredentials()).toThrow(
`[credentials] ${name} uses a retired default value. Configure a unique replacement before starting LibreChat.`,
);
});
it.each([
['JWT_SECRET', '16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef'],
['JWT_REFRESH_SECRET', 'eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8418'],
] as const)('rejects retired default %s values from temporary credentials', (name, value) => {
fs.writeFileSync(tempFile, `${name}=${value}\n`);
expect(() => bootstrapCredentials()).toThrow(
`[credentials] ${name} uses a retired default value. Configure a unique replacement before starting LibreChat.`,
);
});
it('rejects a retired default adopted from a concurrent temporary credential write', () => {
fs.writeFileSync(tempFile, `CREDS_KEY=${'a'.repeat(64)}\nCREDS_IV=${'b'.repeat(32)}\n`);
fs.writeFileSync(
`${tempFile}.lock`,
[
`CREDS_KEY=${'a'.repeat(64)}`,
`CREDS_IV=${'b'.repeat(32)}`,
'JWT_SECRET=16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef',
`JWT_REFRESH_SECRET=${'c'.repeat(64)}`,
].join('\n'),
{ mode: 0o600 },
);
expect(() => bootstrapCredentials()).toThrow(
'[credentials] JWT_SECRET uses a retired default value. Configure a unique replacement before starting LibreChat.',
);
});
it('does not overwrite an explicitly selected environment file', () => {
const environmentFile = path.join(tempDirectory, '.env');
process.env.LIBRECHAT_TEMP_CREDENTIALS_PATH = environmentFile;

View file

@ -11,7 +11,7 @@ export const credentialNames = [
export type CredentialName = (typeof credentialNames)[number];
export type CredentialSource = 'environment' | 'temporary' | 'legacy-default';
export type CredentialSource = 'environment' | 'temporary';
export interface CredentialRuntimeState {
filePath: string;
@ -56,6 +56,18 @@ const legacyCredentialFingerprints: Partial<Record<CredentialName, string[]>> =
JWT_REFRESH_SECRET: ['282ad5f60261639fefed381976b4d0dde52eab5527a1ab2ec75d5be1efa1165b'],
};
const legacyJwtCredentialNames = new Set<CredentialName>(['JWT_SECRET', 'JWT_REFRESH_SECRET']);
function rejectLegacyJwtCredential(name: CredentialName, value: string): void {
if (!legacyJwtCredentialNames.has(name) || !isLegacyCredential(name, value)) {
return;
}
throw new Error(
`[credentials] ${name} uses a retired default value. Configure a unique replacement before starting LibreChat.`,
);
}
function getRuntimeState(): CredentialRuntimeState | undefined {
const runtime = globalThis as typeof globalThis &
Record<symbol, CredentialRuntimeState | undefined>;
@ -248,6 +260,8 @@ function adoptCredentialFile(
if (!isUsableTemporaryCredential(name, value)) {
return false;
}
rejectLegacyJwtCredential(name, value);
}
for (const name of names) {
@ -276,13 +290,15 @@ export function bootstrapCredentials(): CredentialRuntimeState {
for (const name of credentialNames) {
const environmentValue = process.env[name];
if (isConfiguredCredential(environmentValue)) {
sources[name] = isLegacyCredential(name, environmentValue) ? 'legacy-default' : 'environment';
rejectLegacyJwtCredential(name, environmentValue);
sources[name] = 'environment';
continue;
}
missingFromEnvironment.push(name);
const fileValue = file.values[name];
if (isUsableTemporaryCredential(name, fileValue)) {
rejectLegacyJwtCredential(name, fileValue);
process.env[name] = fileValue;
sources[name] = 'temporary';
loadedFromFile.push(name);