Skip to content

fix(api): require admin on /coupons/redeem and /clicks ingest (authz gap) - #61

Open
keithfawcett wants to merge 1 commit into
mainfrom
fix/api-authz-missing-requireadmin
Open

fix(api): require admin on /coupons/redeem and /clicks ingest (authz gap)#61
keithfawcett wants to merge 1 commit into
mainfrom
fix/api-authz-missing-requireadmin

Conversation

@keithfawcett

Copy link
Copy Markdown
Contributor

What

Two server-to-server ingest routes were missing the requireAdmin gate that their sibling /attribution/events route has.

Critical — /coupons/redeem. Gated only by requireAuth + grantScope('events:write'). grantScope is a pass-through for any non-scoped principal (auth.ts:170), and requireAuth admits partner sessions and partner API keys. So a logged-in partner reached the handler and could POST an arbitrary userId/value/code → the handler synthesizes Click+Identity+Event and runs attribution → mints commissions crediting themselves, which then auto-approve and reach payout.

Lower — /clicks. Same shape (grantScope('clicks:write'), no role gate). Impact is smaller because the handler derives partnerId/programId from the Link row rather than caller input, so a partner can't credit themselves — only inflate click counts or create clicks crediting a link's real owner. Still a server-to-server route that shouldn't accept partner principals.

Surfaced by an unsteered Codex review of the whole app.

Fix

Both routes now match /attribution/events: requireAuth → grantScope(...) → requireAdmin. Scoped federation keys (clicks:write / events:write) are unaffected — grantScope rewrites a valid scoped key to admin before requireAdmin runs, so the Network federation path and CRM/Zapier integrations keep working. Only bare partner/non-admin principals are now rejected.

Tests

New regression cases in integration.test.ts: a minted partner key gets 403 on both routes, while the admin key passes auth and reaches the handler (404 on the unknown coupon/link). Full typecheck clean; the two new tests pass alongside the existing integration suite.

Notes

Independent of #60 (branched off main). This is one item from a broader payment/subscription + whole-app audit; the remaining findings (e.g. PartnerProgram RLS, SSRF on webhook/postback URLs, entitlement/revenue leaks) are being triaged separately.

🤖 Generated with Claude Code

…gap)

/coupons/redeem was gated only by requireAuth + grantScope('events:write').
grantScope is a pass-through for any non-scoped principal, so a logged-in
PARTNER (session or partner API key) reached the handler and could forge
conversions — synthesizing Click+Identity+Event and minting commissions
crediting themselves. /clicks had the same shape (grantScope('clicks:write')
with no role gate); lower impact since the handler derives partner/program
from the Link row, but it's still a server-to-server ingest route that must
not accept partner principals.

Both now match the sibling /attribution/events route: requireAuth →
grantScope → requireAdmin. Scoped federation keys (clicks:write /
events:write) still work — grantScope rewrites them to admin before
requireAdmin runs. Added regression tests asserting a partner key gets 403
while admin/scoped reaches the handler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant