Labels / Complexity: bug, security · High — 500 points
Problem
app/api/user-data/route.ts identifies the user purely from a client-supplied header and performs no authentication:
function getWalletAddress(request: NextRequest): string | null {
const walletAddress = request.headers.get('x-wallet-address');
return walletAddress?.trim() || null;
}
GET, PUT, and DELETE all accept that header as the identity, so anyone who knows (or guesses) a wallet address can read, overwrite, or delete that user's bookmarks, drafts, and preferences. lib/api/userData.ts makes this trivially callable by setting x-wallet-address to any string.
The backing store compounds the problem. lib/server/userDataStore.ts keeps every user's snapshot in a single file and read-modify-writes it with no locking:
const filePath = path.join(dataDirectory, 'user-data-store.json');
...
export async function saveUserData(snapshot: UserDataSnapshot): Promise<UserDataSnapshot> {
const store = await readStore();
store[snapshot.walletAddress] = snapshot;
await writeStore(store); // ← whole file rewritten on every save
}
Consequence: (a) an IDOR — any client can read/write/delete any wallet's data by setting the header; (b) concurrent saves read the same file and the last writer wins, silently dropping updates; and (c) the store is a local JSON file that is lost on ephemeral/serverless redeploys and is not durable across instances. Preferences, bookmarks, and drafts are therefore neither private nor reliable.
Root cause
// app/api/user-data/route.ts
const walletAddress = request.headers.get('x-wallet-address'); // ← client-trusted identity
// lib/server/userDataStore.ts
const store = await readStore();
store[snapshot.walletAddress] = snapshot;
await writeStore(store); // ← no locking, whole-file rewrite
Why this is architecturally hard
- The shortcut — adding an auth check — requires a real identity source. The app has no server-side session established for the Stellar wallet flow (
lib/auth/authOptions.ts wires OAuth providers, while the backend authenticates by wallet signature). The fix must decide which identity to trust: a verified JWT from the backend, a NextAuth session, or a wallet-signature challenge, then derive the wallet address from that instead of a header.
- The storage layer is synchronous and file-based, which cannot safely scale beyond one process. A durable fix must move to a database (e.g. Postgres, already in
DATABASE_URL in .env.example) or Redis (already a declared REDIS_URL env var) and decide on a per-user keying scheme.
useUserDataSync (hooks/useUserDataSync.ts) and the drafts flow assume the current snapshot shape; a migration to a real store must preserve the UserDataSnapshot type (types/userData.ts) or update every consumer.
Proposed design
Authenticate the route (reject requests without a valid token and derive the wallet address from the token's walletAddress claim, which the backend puts into the JWT at POST /auth/verify), and replace userDataStore.ts with a database- or Redis-backed store keyed by wallet address. Keep the UserDataSnapshot shape stable so useUserDataSync and the drafts UI keep working.
Downstream impact
This is a frontend-internal change but it must trust the backend's JWT claims (walletAddress in the accessToken payload), so it depends on the backend auth contract. No ABI change to the backend. Repo-local otherwise.
Acceptance criteria
Security
Durability
Tests
Out of scope
Replicating bookmarks/drafts into the backend's Postgres schema is a larger cross-repo task; this issue can use a local durable store (Postgres/Redis) without changing the backend.
Getting started
Files in scope: app/api/user-data/route.ts, lib/server/userDataStore.ts, lib/api/userData.ts, hooks/useUserDataSync.ts.
npm run type-check
npm run lint
Good first files to read: app/api/user-data/route.ts, lib/server/userDataStore.ts, types/userData.ts.
Labels / Complexity: bug, security · High — 500 points
Problem
app/api/user-data/route.tsidentifies the user purely from a client-supplied header and performs no authentication:GET,PUT, andDELETEall accept that header as the identity, so anyone who knows (or guesses) a wallet address can read, overwrite, or delete that user's bookmarks, drafts, and preferences.lib/api/userData.tsmakes this trivially callable by settingx-wallet-addressto any string.The backing store compounds the problem.
lib/server/userDataStore.tskeeps every user's snapshot in a single file and read-modify-writes it with no locking:Consequence: (a) an IDOR — any client can read/write/delete any wallet's data by setting the header; (b) concurrent saves read the same file and the last writer wins, silently dropping updates; and (c) the store is a local JSON file that is lost on ephemeral/serverless redeploys and is not durable across instances. Preferences, bookmarks, and drafts are therefore neither private nor reliable.
Root cause
Why this is architecturally hard
lib/auth/authOptions.tswires OAuth providers, while the backend authenticates by wallet signature). The fix must decide which identity to trust: a verified JWT from the backend, a NextAuth session, or a wallet-signature challenge, then derive the wallet address from that instead of a header.DATABASE_URLin.env.example) or Redis (already a declaredREDIS_URLenv var) and decide on a per-user keying scheme.useUserDataSync(hooks/useUserDataSync.ts) and the drafts flow assume the current snapshot shape; a migration to a real store must preserve theUserDataSnapshottype (types/userData.ts) or update every consumer.Proposed design
Authenticate the route (reject requests without a valid token and derive the wallet address from the token's
walletAddressclaim, which the backend puts into the JWT atPOST /auth/verify), and replaceuserDataStore.tswith a database- or Redis-backed store keyed by wallet address. Keep theUserDataSnapshotshape stable souseUserDataSyncand the drafts UI keep working.Downstream impact
This is a frontend-internal change but it must trust the backend's JWT claims (
walletAddressin theaccessTokenpayload), so it depends on the backend auth contract. No ABI change to the backend. Repo-local otherwise.Acceptance criteria
Security
GET/PUT/DELETE /api/user-datareject requests without a verified token.x-wallet-addressheader alone.Durability
Tests
Out of scope
Replicating bookmarks/drafts into the backend's Postgres schema is a larger cross-repo task; this issue can use a local durable store (Postgres/Redis) without changing the backend.
Getting started
Files in scope:
app/api/user-data/route.ts,lib/server/userDataStore.ts,lib/api/userData.ts,hooks/useUserDataSync.ts.Good first files to read:
app/api/user-data/route.ts,lib/server/userDataStore.ts,types/userData.ts.