fix(channels): reject oversized official channel tokens

Add a 4096-character cap before persisting official channel tokens, map that validation failure to HTTP 400 in the dashboard route, and cover both paths with regression tests.
This commit is contained in:
Tam Nhu Tran
2026-03-25 22:38:15 -04:00
parent 7c91e6ba46
commit ec1417ab7c
4 changed files with 31 additions and 1 deletions
+7
View File
@@ -22,6 +22,8 @@ export interface OfficialChannelTokenStatus {
processEnvAvailable: boolean;
}
const MAX_OFFICIAL_CHANNEL_TOKEN_LENGTH = 4096;
function getResolvedStateDirOverride(
channelId: OfficialChannelId,
envOverrides?: NodeJS.ProcessEnv | null
@@ -293,6 +295,11 @@ export function setConfiguredOfficialChannelToken(
if (!normalized) {
throw new Error(`${envKey} cannot be empty or multiline.`);
}
if (normalized.length > MAX_OFFICIAL_CHANNEL_TOKEN_LENGTH) {
throw new Error(
`${envKey} cannot exceed ${MAX_OFFICIAL_CHANNEL_TOKEN_LENGTH} characters.`
);
}
const envPath = getOfficialChannelEnvPath(channelId);
const currentContent = readFileIfExists(envPath) ?? '';
+2 -1
View File
@@ -178,7 +178,8 @@ router.put('/:channelId/token', (req: Request, res: Response): void => {
res.json({ success: true, tokenConfigured: true, tokenPath });
} catch (error) {
const message = (error as Error).message;
const statusCode = message.includes('cannot be empty') ? 400 : 500;
const statusCode =
message.includes('cannot be empty') || message.includes('cannot exceed') ? 400 : 500;
res.status(statusCode).json({ error: message });
}
});
@@ -56,6 +56,16 @@ describe('official channels token store', () => {
expect(readConfiguredOfficialChannelToken('telegram')).toBe('telegram-secret');
});
it('rejects oversized official channel tokens before writing channel state', () => {
const envPath = getOfficialChannelEnvPath('discord');
expect(() => setConfiguredOfficialChannelToken('discord', 'x'.repeat(4097))).toThrow(
'DISCORD_BOT_TOKEN cannot exceed 4096 characters.'
);
expect(fs.existsSync(envPath)).toBe(false);
expect(hasConfiguredOfficialChannelToken('discord')).toBe(false);
});
it('uses the official state-dir override when one is configured', () => {
const originalDiscordStateDir = process.env.DISCORD_STATE_DIR;
process.env.DISCORD_STATE_DIR = path.join(tempHome, 'discord-state');
@@ -194,4 +194,16 @@ describe('web-server channels-routes', () => {
else delete process.env.DISCORD_BOT_TOKEN;
}
});
it('rejects oversized token writes with a 400 response', async () => {
const response = await putJson(baseUrl, '/api/channels/discord/token', {
token: 'x'.repeat(4097),
});
expect(response.status).toBe(400);
expect(await response.json()).toEqual({
error: 'DISCORD_BOT_TOKEN cannot exceed 4096 characters.',
});
expect(fs.existsSync(path.join(tempHome, '.claude', 'channels', 'discord', '.env'))).toBe(false);
});
});