diff --git a/.changeset/tough-guests-drive.md b/.changeset/tough-guests-drive.md new file mode 100644 index 000000000..9489feeb1 --- /dev/null +++ b/.changeset/tough-guests-drive.md @@ -0,0 +1,5 @@ +--- +"@sapiom/harness": patch +--- + +Compare the /ingest bearer token in constant time, matching the /api and WS upgrade paths. diff --git a/packages/harness/src/server/ingest.test.ts b/packages/harness/src/server/ingest.test.ts index f3314b49b..3c15e62c5 100644 --- a/packages/harness/src/server/ingest.test.ts +++ b/packages/harness/src/server/ingest.test.ts @@ -88,6 +88,12 @@ describe("createIngestRouter", () => { expect(stored).toHaveLength(0); }); + it("rejects requests with no Authorization header at all", async () => { + const res = await postIngest(baseUrl, { hookEvent: "SessionStart" }, ""); + expect(res.status).toBe(401); + expect(stored).toHaveLength(0); + }); + it("responds 200 immediately and processes asynchronously", async () => { const res = await postIngest(baseUrl, { hookEvent: "UserPromptSubmit", diff --git a/packages/harness/src/server/ingest.ts b/packages/harness/src/server/ingest.ts index 4e82e1f6a..104130cc9 100644 --- a/packages/harness/src/server/ingest.ts +++ b/packages/harness/src/server/ingest.ts @@ -13,6 +13,7 @@ import express, { type Router } from "express"; import type { AnalyticsEvent, HarnessKind } from "../shared/types.js"; import type { NormalizeContext } from "../core/collector/normalizer.js"; import { createSeqCounter, type SeqCounter } from "../core/collector/seq.js"; +import { timingSafeEqualString } from "./auth.js"; export interface IngestSessionContext { harness: HarnessKind; @@ -162,7 +163,7 @@ export function createIngestRouter(deps: IngestDeps): Router { router.post("/ingest", (req, res) => { const token = bearerToken(req.headers.authorization); - if (token !== deps.ingestToken) { + if (!timingSafeEqualString(token ?? "", deps.ingestToken)) { res.status(401).json({ ok: false }); return; }