Skip to content

[Security] Add minimal wallet signature auth to /api/upload-cover #1136

@realproject7

Description

@realproject7

Problem

`/api/upload-cover` has no authentication. Anyone can upload images to our Filebase bucket. The in-memory IP rate limit resets on serverless cold starts and is easily bypassed.

Scope — Minimal Change Only

Add EIP-191 wallet signature requirement. Nothing else — no Supabase rate limit tables, no new infrastructure.

API Change

Update `POST /api/upload-cover` to require two additional fields in the multipart FormData:

  • `message` — plaintext message string
  • `signature` — EIP-191 hex signature

Message format:
```
PlotLink: Upload cover image
Timestamp: {unix_ms}
```

Server-side verification:
```ts
import { recoverMessageAddress } from "viem";

// 1. Extract message and signature from FormData
// 2. If missing, return 401
// 3. Parse timestamp from message, reject if > 5 minutes old
// 4. Recover signer address from signature
// 5. Switch in-memory rate limit key from IP to recovered wallet address
// 6. Proceed with existing upload logic
```

Keep existing in-memory rate limit but key by recovered wallet address instead of IP. Keep max 5/min — simple and sufficient.

Frontend Changes (MUST ship in same PR)

Both callers must be updated to sign before uploading. If the API deploys without frontend changes, uploads will break.

`src/components/StoryEditPanel.tsx`

Already has `useSignMessage` imported and `signMessageAsync` available (line 35). Add:
```ts
const timestamp = Date.now();
const message = `PlotLink: Upload cover image
Timestamp: ${timestamp}`;
const signature = await signMessageAsync({ message });
formData.append("message", message);
formData.append("signature", signature);
```

`src/app/create/page.tsx`

Does NOT have `useSignMessage` yet. Add the import and hook, then same signing pattern before the fetch call on line 167.

What NOT to do

  • Do NOT create a Supabase table for rate limiting
  • Do NOT change the message format beyond what's specified above
  • Do NOT add any other auth mechanisms
  • Do NOT touch `/api/upload` (text upload) — out of scope

Security Analysis

What this prevents:

  • Anonymous spam uploads (attacker needs a real wallet key to sign)
  • Automated abuse (each upload requires a fresh signature within 5-min window)

Accepted risks:

  • Same signature can upload multiple files within 5-min window → mitigated by rate limit
  • In-memory rate limit resets on cold starts → acceptable; signature is the primary defense
  • Uploaded images persist on IPFS even if never associated with a storyline → unavoidable with IPFS; low impact since images are inert without DB association

What this does NOT break:

  • PlotLink OWS does not currently call this endpoint → no impact
  • Both frontend callers are updated in the same PR → atomic Vercel deploy, no window of breakage

Files

  • `src/app/api/upload-cover/route.ts` — add signature verification, switch rate limit key
  • `src/components/StoryEditPanel.tsx` — sign before upload (~5 lines)
  • `src/app/create/page.tsx` — add `useSignMessage` hook + sign before upload (~10 lines)

Edge Case: Wallet Not Connected on Create Page

On the create page, users may try to upload a cover image before connecting their wallet. Since signing requires a connected wallet, handle this gracefully:

  • If wallet is not connected when cover is selected, show a message: "Connect your wallet to upload a cover image"
  • Alternatively, defer the upload until the publish step (but this complicates the flow — prefer the connect-first approach)
  • The edit panel does not have this issue since it only appears for connected authors

Acceptance Criteria

  • API rejects uploads without signature (401)
  • API rejects expired timestamps (> 5 min)
  • Rate limit keyed by recovered wallet address (not IP)
  • StoryEditPanel signs before uploading
  • Create page signs before uploading
  • All three changes in the SAME PR (atomic deploy)
  • Existing upload flow works exactly as before for authenticated users

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/T3Assigned to T3 builder agent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions