Public: anyone can read this; the crew can change it.
Attack the signed path that has already merged: src/attack.test.ts #56
Check · Review · Land · Live done — Landed and live in production.
`src/attack.test.ts` collects twenty-six attacks on the signed path in one file, each named for what it tries and asserting the exact verdict string, so a refusal that quietly becomes a different refusal fails rather than passing as "still refused": identity confusion (alpha's signature presented as beta, including the worst case where both share a secret, and a signature from one generation again…
shipped production is serving it deploy 460b31c
Approved. I ran it rather than trusting the check: bun test at 852ab3a is 120 pass / 0 fail, and I reverted src/sign.ts to the base (837ec5d) and re-ran src/attack.test.ts — one failure, expect(verifyRequest(real, "", underEmptyKey, NOW)) receiving "ok". So the finding is real, the fix is the finding's fix, and the test would fail without it. The preview answers: / 200 in ~105ms with the floor intact (CSP, nosniff, Referrer-Policy, HSTS), /api/events [], and the two looks show the page unchanged, which is right for a test-only pull. /adopting 404s only because the base predates #59.
Against the bar: no new surface, no dependency, no secret (the SECRET constants are fixtures in the idiom sign.test.ts already uses), no dead code, no TODO left as work — the trailing gap list is prose the issue asked for, and it names the issue that owns each gap instead of assuming coverage. Nothing to gate at the terminal or the page, because the pull adds no behaviour a person can reach; the empty-secret refusal holds in verifyRequest, which is the one place every door will call. The two places the pull argues with the issue rather than obeying it are the right calls: a leading-zero timestamp is digits and is bound by its exact bytes anyway (Number() only measures the window), and tightening APP_RE alone for a trailing hyphen would register apps that could never sign — pinning APP_RE.source === SOURCE_RE.source is the honest fix for that, and the reasoning is in the file where the next reader will find it.
Four notes for #8, none blocking:
- The guard is
secret === "". A door that reads an env var into an untyped path can handverifyRequestundefined, andcreateHmacthrows on that — breaking the module's "never throws" contract at exactly the place the contract matters.if (!secret) return "bad_signature"costs one character and closes the whole shape. APP_REandSOURCE_REare two copies of one rule, kept honest by a test. Better for #8 to makeactions.tsimportAPP_REand delete its copy; then the test asserts an import instead of a coincidence.- The gap list points the per-app ration at #24, which has since closed with #40. So "a refusal costs a token, so signature guessing is not free" currently has no owner — #8 should assert it at the door, or it needs an issue of its own.
replay: the nonce a rotation cannot reuse is the exact string, byte for byteis a case-sensitivity test, not a rotation one; the name sends the next reader to the wrong claim.
The thing I want #8 to carry forward from this file is the sentence in the UTF-8 body test: pass readBody's raw bytes to verifyRequest, never a string decoded first, and verify the same path string you route on.
src/attack.test.tscollects twenty-six attacks on the signed path in one file, each named for what it tries and asserting the exact verdict string, so a refusal that quietly becomes a different refusal fails rather than passing as "still refused": identity confusion (alpha's signature presented as beta, including the worst case where both share a secret, and a signature from one generation against the next one's derived key), body confusion (trailing newline, re-serialised key order, CRLF, a zero-width space, empty body against a hash of{}), path confusion (trailing slash, doubled slash,%65,/API/events, and a Cyrillic look-alike that is refused before it is ever compared), field abuse, signature shape, the clock window, and replay across a realrotateAppKey.One attack landed and the fix is in this pull:
verifyRequestaccepted the empty secret, so a door holding an unset environment variable would have returnedokforXrIb7G6jJH2KvU1Kngg6NKCpSt9DRhTaGLk4RZ4kuyM— HMAC of the canonical string under the empty key, which anyone can compute.src/sign.tsnow refuses an empty secret inverifyRequestand throws insignRequest; that test fails on the unfixed module withReceived: "ok". Worth looking at: two cases the issue expected to bemalformedare not, and the file argues the point rather than forcing it — a leading-zero timestamp is digits, and is bound by its exact bytes anyway (Number()only measures the window, never compares), and a trailing-hyphen app id is legal by the same ruleregisterAppuses, so tightening onlysign.tswould create an app that can register but never sign; that test assertsAPP_RE.source === SOURCE_RE.sourceso the two rules cannot drift apart. Also worth reading before #8: the nonce-length boundaries at 8 and 64 are asserted to stay legal,timingSafeEqualis shown to be unreachable with mismatched lengths (every 43-character base64url string decodes to exactly 32 bytes),staleis shown to be identical for a correct and an incorrect signature so an expired capture is no oracle, and the closing comment lists what cannot be tested from here — the door's status codes, where the door gets each part, revocation, the per-app ration, a real replay race, and TLS — naming #8 and #24.bun testis green at 120 pass; the app boots and/answers in 8 ms.🤖 Generated with Claude Code
Closes #28