Epic: #1229 · Spec: §6.2, §3.1, R22 · Type: CODE · Estimate: 0.4 day · Depends on: T2.4a
Scope
Critical security: close the activation-signature-replay attack surface on referral mutation.
Within the 10-min freshness window, an intercepted activation {message, signature} pair can currently be replayed against /register-referral or /referral-code POST (which use v1's weaker verifyWalletOwnership — signature-recovery only). Attacker could bind a victim wallet under attacker's referral code, inflating attacker multiplier (R2 surface).
Files to migrate
src/app/api/airdrop/register-referral/route.ts (POST)
src/app/api/airdrop/referral-code/route.ts (POST only — see preservation note below)
- AUDIT:
src/app/api/airdrop/leaderboard/route.ts and any other endpoint that accepts { message, signature }. Migrate similarly. (Likely caught by T2.5f for /leaderboard.)
🚨 PRESERVE: /api/airdrop/referral-code GET stays unauthenticated (RE1 round-12 main-app risk mitigation)
The GET endpoint at /api/airdrop/referral-code?address=X returns {code, is_farcaster_username} WITHOUT auth (current behavior in src/app/api/airdrop/referral-code/route.ts:14-37). It is consumed by src/hooks/useReferralCode.ts which powers src/components/ShareButtons.tsx:15, which renders on every story page (src/app/story/[storylineId]/page.tsx:239).
Do NOT add auth to GET. Only migrate POST. Response shape MUST remain { code: string | null, is_farcaster_username?: boolean }. Any change to GET breaks ?ref=CODE share-link tracking across the entire main app.
Change (POST only)
Replace:
import { verifyWalletOwnership } from '@/lib/airdrop/verify-wallet';
const address = await verifyWalletOwnership(message, signature);
With:
import { verifySiweRequest } from '@/lib/airdrop/siwe-verify';
const result = await verifySiweRequest(message, signature);
if (!result.ok) return new Response(result.error, { status: 401 });
const { address } = result;
POST response shape unchanged; only the auth verification path changes.
Acceptance
Dependencies
T2.4a (siwe-verify lib must exist)
Epic: #1229 · Spec: §6.2, §3.1, R22 · Type: CODE · Estimate: 0.4 day · Depends on: T2.4a
Scope
Critical security: close the activation-signature-replay attack surface on referral mutation.
Within the 10-min freshness window, an intercepted activation
{message, signature}pair can currently be replayed against/register-referralor/referral-code POST(which use v1's weakerverifyWalletOwnership— signature-recovery only). Attacker could bind a victim wallet under attacker's referral code, inflating attacker multiplier (R2 surface).Files to migrate
src/app/api/airdrop/register-referral/route.ts(POST)src/app/api/airdrop/referral-code/route.ts(POST only — see preservation note below)src/app/api/airdrop/leaderboard/route.tsand any other endpoint that accepts{ message, signature }. Migrate similarly. (Likely caught by T2.5f for/leaderboard.)🚨 PRESERVE:
/api/airdrop/referral-codeGET stays unauthenticated (RE1 round-12 main-app risk mitigation)The GET endpoint at
/api/airdrop/referral-code?address=Xreturns{code, is_farcaster_username}WITHOUT auth (current behavior insrc/app/api/airdrop/referral-code/route.ts:14-37). It is consumed bysrc/hooks/useReferralCode.tswhich powerssrc/components/ShareButtons.tsx:15, which renders on every story page (src/app/story/[storylineId]/page.tsx:239).Do NOT add auth to GET. Only migrate POST. Response shape MUST remain
{ code: string | null, is_farcaster_username?: boolean }. Any change to GET breaks?ref=CODEshare-link tracking across the entire main app.Change (POST only)
Replace:
With:
POST response shape unchanged; only the auth verification path changes.
Acceptance
/api/airdrop/referral-coderemains unauthenticated — verified bycurl ${BASE}/api/airdrop/referral-code?address=0x...with no auth headers, returns 200 +{code: ...}shapeShareButtonson a sample story page still appends?ref=CODEafter this PR deploys to stagingverifyWalletOwnershipcall in any v5 signed mutation endpoint/register-referral11 min after issue → 401Dependencies
T2.4a (
siwe-verifylib must exist)