From 42ad28fe50ac887570bfc8dcc0b5f069bb2f5253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Thu, 22 May 2025 12:49:43 +0800 Subject: [PATCH] fix: mitigate timing based attack for api key --- src/utils/verifyApiKey.test.ts | 26 ++++++++++++++++++++++++++ src/utils/verifyApiKey.ts | 14 +++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/utils/verifyApiKey.test.ts diff --git a/src/utils/verifyApiKey.test.ts b/src/utils/verifyApiKey.test.ts new file mode 100644 index 000000000..22138fb16 --- /dev/null +++ b/src/utils/verifyApiKey.test.ts @@ -0,0 +1,26 @@ +import { apiKeyMatches } from './verifyApiKey' + +describe('verifyApiKey', () => { + test.each([ + // false if apikey is undefined + { userAuth: 'some-pw', apiKey: '', expected: false }, + // false if user auth is undefined + { userAuth: '', apiKey: 'some-pw', expected: false }, + + // false if mismatch with different length + { userAuth: 'some-pw-2', apiKey: 'some-pw', expected: false }, + { userAuth: 'short', apiKey: 'some-pw', expected: false }, + { userAuth: 'looooooooooooong', apiKey: 'some-pw', expected: false }, + { userAuth: 'sameeee', apiKey: 'some-pw', expected: false }, + + // true if actually matches + { + userAuth: 'ep5oWe3Aingi2chah9phai5eiKeisahviedei1geiNgaf4Neuv', + apiKey: 'ep5oWe3Aingi2chah9phai5eiKeisahviedei1geiNgaf4Neuv', + expected: true, + }, + ])('testing %s against %s, expected %s', ({ userAuth, apiKey, expected }) => { + const result = apiKeyMatches(userAuth, apiKey) + expect(result).toBe(expected) + }) +}) diff --git a/src/utils/verifyApiKey.ts b/src/utils/verifyApiKey.ts index f5747704d..b1e24e479 100644 --- a/src/utils/verifyApiKey.ts +++ b/src/utils/verifyApiKey.ts @@ -1,5 +1,6 @@ import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from 'fastify' import { getConfig } from './config' +import { timingSafeEqual } from 'node:crypto' const config = getConfig() @@ -12,8 +13,19 @@ export const verifyApiKey = ( return reply.code(401).send('Unauthorized') } const { authorization } = request.headers - if (authorization !== config.API_KEY) { + if (!apiKeyMatches(authorization, config.API_KEY)) { return reply.code(401).send('Unauthorized') } done() } + +export function apiKeyMatches(authorization: string, apiKey: string | undefined): boolean { + if (!apiKey) return false + if (!authorization) return false + if (authorization.length > apiKey.length) return false + + // timingSafeEqual needs both buffers to be the same length + const sameLengthAuth = authorization.padEnd(apiKey.length, ' ') + + return timingSafeEqual(Buffer.from(sameLengthAuth), Buffer.from(apiKey)) +}