From a62c264ddfcbaf694dd849d4c365a6555bf49704 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 13:23:31 -0500 Subject: [PATCH] fix: harden public routine trigger auth --- server/src/routes/routines.ts | 4 ++-- server/src/services/routines.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/server/src/routes/routines.ts b/server/src/routes/routines.ts index dbd860b4..41452420 100644 --- a/server/src/routes/routines.ts +++ b/server/src/routes/routines.ts @@ -17,7 +17,7 @@ export function routineRoutes(db: Db) { const router = Router(); const svc = routineService(db); - async function assertCanManageCompanyRoutine(req: Request, companyId: string, assigneeAgentId?: string | null) { + function assertCanManageCompanyRoutine(req: Request, companyId: string, assigneeAgentId?: string | null) { assertCompanyAccess(req, companyId); if (req.actor.type === "board") return; if (req.actor.type !== "agent" || !req.actor.agentId) throw unauthorized(); @@ -47,7 +47,7 @@ export function routineRoutes(db: Db) { router.post("/companies/:companyId/routines", validate(createRoutineSchema), async (req, res) => { const companyId = req.params.companyId as string; - await assertCanManageCompanyRoutine(req, companyId, req.body.assigneeAgentId); + assertCanManageCompanyRoutine(req, companyId, req.body.assigneeAgentId); const created = await svc.create(companyId, req.body, { agentId: req.actor.type === "agent" ? req.actor.agentId : null, userId: req.actor.type === "board" ? req.actor.userId ?? "board" : null, diff --git a/server/src/services/routines.ts b/server/src/services/routines.ts index f0c65c4a..06adcaa8 100644 --- a/server/src/services/routines.ts +++ b/server/src/services/routines.ts @@ -1016,7 +1016,14 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup const secretValue = await resolveTriggerSecret(trigger, routine.companyId); if (trigger.signingMode === "bearer") { const expected = `Bearer ${secretValue}`; - if (!input.authorizationHeader || input.authorizationHeader.trim() !== expected) { + const provided = input.authorizationHeader?.trim() ?? ""; + const expectedBuf = Buffer.from(expected); + const providedBuf = Buffer.alloc(expectedBuf.length); + providedBuf.write(provided.slice(0, expectedBuf.length)); + const valid = + provided.length === expected.length && + crypto.timingSafeEqual(providedBuf, expectedBuf); + if (!valid) { throw unauthorized(); } } else {