Skip to content

Commit a8d1c4b

Browse files
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>
1 parent cd19834 commit a8d1c4b

1 file changed

Lines changed: 7 additions & 2 deletions

File tree

server/src/services/routines.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,16 +1272,21 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
12721272
} else if (trigger.signingMode === "github_hmac") {
12731273
const secretValue = await resolveTriggerSecret(trigger, routine.companyId);
12741274
const rawBody = input.rawBody ?? Buffer.from(JSON.stringify(input.payload ?? {}));
1275+
// Accept X-Hub-Signature-256 (GitHub/Sentry) or fall back to the
1276+
// generic X-Paperclip-Signature header so operators can use github_hmac
1277+
// mode with either header convention.
12751278
const providedSignature = (input.hubSignatureHeader ?? input.signatureHeader)?.trim() ?? "";
12761279
if (!providedSignature) throw unauthorized();
12771280
const expectedHmac = crypto
12781281
.createHmac("sha256", secretValue)
12791282
.update(rawBody)
12801283
.digest("hex");
12811284
const normalizedSignature = providedSignature.replace(/^sha256=/, "");
1285+
const normalizedBuf = Buffer.from(normalizedSignature);
1286+
const expectedBuf = Buffer.from(expectedHmac);
12821287
const valid =
1283-
normalizedSignature.length === expectedHmac.length &&
1284-
crypto.timingSafeEqual(Buffer.from(normalizedSignature), Buffer.from(expectedHmac));
1288+
normalizedBuf.length === expectedBuf.length &&
1289+
crypto.timingSafeEqual(normalizedBuf, expectedBuf);
12851290
if (!valid) throw unauthorized();
12861291
} else if (trigger.signingMode === "bearer") {
12871292
const secretValue = await resolveTriggerSecret(trigger, routine.companyId);

0 commit comments

Comments
 (0)