Unauthenticated IDOR on /api/notes Discloses Private User Data (Bank PINs, SSNs)
idor · server.js:48-53
15b34d5cc6 by 42nights on 2026-06-03Summary
The /api/notes handler at server.js:48 looks up a note solely by the attacker-controlled numeric `id` query parameter and returns the full record, including notes flagged `private: true`. There is no authentication, session validation, or check that the requesting user matches the note's `owner` field. The endpoint should require an authenticated session and verify that `note.owner` equals the caller's identity (or that the note is non-private) before returning it; instead it returns any record matching the supplied id.
Impact. Any unauthenticated remote attacker can enumerate `id=1,2,3,...` and read every user's notes, including content explicitly marked private such as bank PINs and SSNs (validated: `curl /api/notes?id=2` returned alice's private note containing `bank pin 4821`). This is a direct, mass disclosure of PII and credentials with no barrier — trivially scriptable and usable for fraud, account takeover, or identity theft.
Vulnerable code — server.js
🔴 if (parsed.pathname === "/api/notes") {🔴 const id = Number(q.id || 1);🔴 const note = NOTES.find((n) => n.id === id);🔴 if (!note) return send(res, 404, "no such note");🔴 return send(res, 200, JSON.stringify(note), "application/json");🔴 }
Working exploit
curl -s 'http://127.0.0.1:4600/api/notes?id=2'
Exploit transcript
HTTP 200 Content-Type: application/json
{"id":2,"owner":"alice","text":"alice PRIVATE: bank pin 4821","private":true}Confirmed: HTTP 200 application/json body: {"id":2,"owner":"alice","text":"alice PRIVATE: bank pin 4821","private":true} retrieved via unauthenticated GET /api/notes?id=2
CVSS v3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N→ 7.5 (high)Recommended fix
AI-suggested fix — review before applying (derived from analysis of untrusted repo content):
Require authentication and enforce ownership before returning the note:
```diff
if (parsed.pathname === "/api/notes") {
- const id = Number(q.id || 1);
- const note = NOTES.find((n) => n.id === id);
- if (!note) return send(res, 404, "no such note");
- return send(res, 200, JSON.stringify(note), "application/json");
+ const user = authenticate(req); // validate session/token
+ if (!user) return send(res, 401, "authentication required");
+ const id = Number(q.id);
+ if (!Number.isInteger(id)) return send(res, 400, "invalid id");
+ const note = NOTES.find((n) => n.id === id);
+ if (!note) return send(res, 404, "no such note");
+ if (note.owner !== user.username && note.private) {
+ return send(res, 403, "forbidden");
+ }
+ return send(res, 200, JSON.stringify(note), "application/json");
}
```
Prefer scoping the query to the caller (e.g., `NOTES.find(n => n.id === id && n.owner === user.username)`) so existence of others' notes is not revealed.Ship the fix
Rook found it and proved it. Hand the finding — with its working exploit as a failing test — to Otis to write the fix PR.