fix(server): use Buffer.length for timing-safe HMAC comparison and document header fallback

Compare Buffer byte lengths instead of string character lengths before
timingSafeEqual to avoid potential mismatch with multi-byte input.
Add comment explaining the hubSignatureHeader ?? signatureHeader fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antonio 2026-04-06 16:26:24 -03:00
parent cd19834fab
commit a8d1c4b596

View file

@ -1272,6 +1272,9 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
} else if (trigger.signingMode === "github_hmac") {
const secretValue = await resolveTriggerSecret(trigger, routine.companyId);
const rawBody = input.rawBody ?? Buffer.from(JSON.stringify(input.payload ?? {}));
// Accept X-Hub-Signature-256 (GitHub/Sentry) or fall back to the
// generic X-Paperclip-Signature header so operators can use github_hmac
// mode with either header convention.
const providedSignature = (input.hubSignatureHeader ?? input.signatureHeader)?.trim() ?? "";
if (!providedSignature) throw unauthorized();
const expectedHmac = crypto
@ -1279,9 +1282,11 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
.update(rawBody)
.digest("hex");
const normalizedSignature = providedSignature.replace(/^sha256=/, "");
const normalizedBuf = Buffer.from(normalizedSignature);
const expectedBuf = Buffer.from(expectedHmac);
const valid =
normalizedSignature.length === expectedHmac.length &&
crypto.timingSafeEqual(Buffer.from(normalizedSignature), Buffer.from(expectedHmac));
normalizedBuf.length === expectedBuf.length &&
crypto.timingSafeEqual(normalizedBuf, expectedBuf);
if (!valid) throw unauthorized();
} else if (trigger.signingMode === "bearer") {
const secretValue = await resolveTriggerSecret(trigger, routine.companyId);