fix(security): compare API token in constant time

Length-guarded timingSafeEqual avoids leaking token information through response timing on the auth path.
This commit is contained in:
tiennm99 committed 2026-07-25 01:17:18 +07:00
1 parent 5048fff952
commit ccb91adef4
1 file changed
+16 -1
+16 -1
View File
@@ -1,8 +1,23 @@
import {timingSafeEqual} from 'node:crypto';
import {ZodError} from 'zod';
import {isRenderTimeoutError} from '../lib/render-errors.js';
import {pickWinnerIndex} from '../lib/winner.js';
import {parseWheelRequest} from '../schemas/wheel-request.js';
/**
* Length-guarded constant-time string comparison so token checks do not leak
* information through response timing.
*
* @param {string} a
* @param {string} b
* @returns {boolean}
*/
const safeEqual = (a, b) => {
const aBuffer = Buffer.from(a);
const bBuffer = Buffer.from(b);
return aBuffer.length === bBuffer.length && timingSafeEqual(aBuffer, bBuffer);
};
/**
* @typedef {import('../config.js').AppConfig} AppConfig
* @typedef {import('../schemas/wheel-request.js').WheelRenderRequest} WheelRenderRequest
@@ -31,7 +46,7 @@ export const registerGifRoute = async (app, deps) => {
app.post('/api/gif', async (request, reply) => {
if (deps.config.apiToken) {
const expected = `Bearer ${deps.config.apiToken}`;
if (request.headers.authorization !== expected) {
if (!safeEqual(request.headers.authorization ?? '', expected)) {
return reply.code(401).send({error: 'unauthorized'});
}
}