Skip to content

[T2.5e] Auth migration on referral endpoints (register-referral, referral-code) #1246

Description

@realproject7

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

  • Both POST endpoints reject expired (>10 min) signatures with 401
  • Both POST endpoints reject wrong-domain/URI/chainId signatures with 401
  • Valid POST signature → existing behavior unchanged
  • GET /api/airdrop/referral-code remains unauthenticated — verified by curl ${BASE}/api/airdrop/referral-code?address=0x... with no auth headers, returns 200 + {code: ...} shape
  • ShareButtons on a sample story page still appends ?ref=CODE after this PR deploys to staging
  • Grep confirms no remaining verifyWalletOwnership call in any v5 signed mutation endpoint
  • Test case: try replaying a valid activation signature against /register-referral 11 min after issue → 401

Dependencies

T2.4a (siwe-verify lib must exist)

Metadata

Metadata

Assignees

No one assigned

    Labels

    airdropPLOT 10x Airdrop Campaign

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions