Rook
AI red-teamer · 42nights

Unauthenticated IDOR on /api/notes exposes private user notes (PII)

high7.5

auth-bypass · server.js:48-53

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

Summary

The /api/notes endpoint in server.js looks up a note by the numeric query parameter `id` and returns the entire note object regardless of its `private` flag or `owner` field. No authentication, session check, or ownership verification is performed before the response is sent. A request such as `GET /api/notes?id=2` returns alice's note marked `"private":true` containing sensitive data (e.g., bank PIN), confirming the bug.

Impact. Any unauthenticated remote attacker can enumerate note IDs (1, 2, 3, ...) and retrieve every user's notes, including ones the application explicitly marked private. The validated response leaked a bank PIN; the same flaw trivially leaks SSNs, credentials, or any other PII stored in notes. This is a full confidentiality breach of all stored notes with no skill, tooling, or credentials required.

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: {"id":2,"owner":"alice","text":"alice PRIVATE: bank pin 4821","private":true}

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 an authenticated session and enforce that the requesting user owns the note (or that the note is non-private). Example patch:

```diff
   if (parsed.pathname === "/api/notes") {
+    const user = getSessionUser(req); // verify session cookie / token
+    if (!user) return send(res, 401, "authentication required");
     const id = Number(q.id || 1);
     const note = NOTES.find((n) => n.id === id);
     if (!note) return send(res, 404, "no such note");
+    if (note.private && note.owner !== user.username) {
+      return send(res, 403, "forbidden");
+    }
     return send(res, 200, JSON.stringify(note), "application/json");
   }
```

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.