AntFleet

Anatomy · 9b74dece-0

action query param does not distinguish enable vs missing — disable-link replay flips on

mediumbugclosed in a58382a
repo e24ef98c·PR #9·reviewed 1 week ago·closed 1 week ago

The vulnerable code

apps/web/app/api/opt-in/route.ts:36-39

36 const token = url.searchParams.get("t");
37 const actionParam = url.searchParams.get("action");
38 const action: OptInAction = actionParam === "disable" ? "disable" : "enable";
39

The reasoning

Opus

action query param does not distinguish enable vs missing — disable-link replay flips on

mediumbughigh
  • apps/web/app/api/opt-in/route.ts:36-39
The route treats any value of `action` other than the exact string "disable" as "enable". This means typos (action=disabled, action=DISABLE, action=off) silently re-enable public receipts even when the user clearly intended to disable. Since the disable link is the user's main self-serve reversal channel and the same token round-trips both flows, a small typo flips the privacy bit the wrong way and emits a public_receipts_enabled audit event. There is no explicit allowlist check or 400 for unknown actions.

Recommendation

Reject unknown action values (return 400) or at minimum case-fold/whitelist {enable,disable}. Treating missing-or-unknown as enable is fine for the default link case, but mixed-case or near-miss tokens should not silently invert the user's intent.

GPT-5

Opt-in route 500s if OPTIN_HMAC_SECRET is missing (no graceful handling)

mediumbughigh
  • apps/web/lib/optin-token.ts:1-200
  • apps/web/app/api/opt-in/route.ts:1-200
verifyTokenDetailed() calls getSecret(), which throws if OPTIN_HMAC_SECRET is unset. handleOptIn() calls verifyTokenDetailed() without try/catch, so the request will crash with a 500 and no targeted error/log context. This is a server misconfiguration path that should be handled gracefully to aid operations and avoid noisy 5xx.

Recommendation

- In the route, guard the token verification with a try/catch that logs a clear server-misconfig event and returns a 500 with a generic error page. - Alternatively, validate OPTIN_HMAC_SECRET at module init of the route (or app startup) and fail fast with a clear boot-time error, preventing the app from serving until the secret is present. - Example: at top-level of route.ts, read process.env.OPTIN_HMAC_SECRET and if missing, log and export a GET that returns a 500 error page with a friendly message (no details about secrets). - Consider adding a healthcheck that asserts required env vars are present.

The agreement

Both frontier models flagged this within the same line range. AntFleet's unanimous gate fired — the finding posted on the PR. Closed in a58382a.

The fix

36 const token = url.searchParams.get("t");
37 const actionParam = url.searchParams.get("action");
38 const action: OptInAction = actionParam === "disable" ? "disable" : "enable";
39

Closure

Closed 1 week ago

SHA: a58382a1c8934544d327ad62fd4c9c54b187d8ef

View closure receipt on GitHub →

Tweet thread template

tweet 1 of 8170 / 280

Two frontier models reviewed PR #9 on e24ef98c. Both found this bug: medium bug: action query param does not distinguish enable vs missing — disable-link replay flips on

tweet 2 of 8124 / 280

The vulnerable code (apps/web/app/api/opt-in/route.ts:36-39): (full snippet at https://www.antfleet.dev/anatomy/9b74dece-0)

tweet 3 of 8280 / 280

What Opus saw: "The route treats any value of `action` other than the exact string "disable" as "enable". This means typos (action=disabled, action=DISABLE, action=off) silently re-enable public receipts even when the user clearly intended to disable. Since the disable link is …

tweet 4 of 8280 / 280

What GPT-5 saw: "verifyTokenDetailed() calls getSecret(), which throws if OPTIN_HMAC_SECRET is unset. handleOptIn() calls verifyTokenDetailed() without try/catch, so the request will crash with a 500 and no targeted error/log context. This is a server misconfiguration path that…

tweet 5 of 897 / 280

Both flagged the same line range. AntFleet's unanimous gate fired — the finding posted on the PR.

tweet 6 of 893 / 280

The fix landed in commit a58382a: (view diff at https://www.antfleet.dev/anatomy/9b74dece-0)

tweet 7 of 881 / 280

AntFleet reviews every PR with two frontier models. Only unanimous findings post.

tweet 8 of 877 / 280

Full anatomy + reasoning + diffs: https://www.antfleet.dev/anatomy/9b74dece-0

Paste into X composer one tweet at a time. X has no multi-tweet intent API.