[{"data":1,"prerenderedAt":387},["ShallowReactive",2],{"blog-diagrams/idor-the-missing-branch":3},{"id":4,"title":5,"body":6,"category":355,"date":356,"dateModified":356,"description":357,"draft":358,"extension":359,"faq":360,"featured":358,"headerVariant":370,"image":371,"keywords":373,"meta":374,"navigation":177,"ogDescription":375,"ogTitle":376,"path":377,"readTime":378,"schemaOrg":379,"schemaType":380,"seo":381,"sitemap":382,"stem":383,"tags":384,"twitterCard":385,"__hash__":386},"blog/blog/diagrams/idor-the-missing-branch.md","IDOR, Drawn: The Branch That Is Not There (Diagram)",{"type":7,"value":8,"toc":349},"minimark",[9,13,16,288,294,300,306,312,315,333,345],[10,11,12],"p",{},"Change 1043 to 1044 in the address bar and press enter. If you get somebody else's invoice back, with a completely normal 200 OK, that is an IDOR, and it is the single most common serious bug we find in apps built with AI coding tools.",[10,14,15],{},"What makes it hard to spot in a code review is that there's nothing wrong on the screen. The code does exactly what it says. The problem is a line that was never written.",[17,18,25,30,37,48,54,64,75,88,92,95,98,102,278],"diagram",{"alt":19,"caption":20,"height":21,"href":22,"src":23,"width":24},"A flowchart of an IDOR bug. A request for invoice 1043 has its id read straight from the URL into a query that filters on id alone, which returns another customer's invoice with a 200 OK. A dotted branch shows the ownership condition that was never written, which would have answered 404 for a row belonging to someone else.","Nothing here is wrong. Something here is absent.",1364,"/blog/vulnerabilities/idor","/diagrams/idor-the-missing-branch.png",1572,[26,27,29],"h2",{"id":28},"reading-the-diagram","Reading the diagram",[10,31,32,36],{},[33,34,35],"strong",{},"The top box, in red."," A request arrives for invoice 1043. The red says what red always says in these diagrams: anyone can send this. Nothing stops a signed-in customer typing a different number, and nothing should, because that's just a URL.",[10,38,39,42,43,47],{},[33,40,41],{},"The grey box."," Here's the query your AI assistant wrote. ",[44,45,46],"code",{},"WHERE id = 1043",". Read it on its own and it's correct: you asked for the invoice with that id, and that's the invoice with that id.",[10,49,50,53],{},[33,51,52],{},"The solid red arrow, going left."," This is what actually happens when you deploy. The row comes back, your API serializes it, the customer sees an invoice that isn't theirs. Status code 200. No error in your logs, no alert, nothing to notice. It looks exactly like a successful request because it is one.",[10,55,56,59,60,63],{},[33,57,58],{},"The dotted arrow, going right."," This is the whole diagram. That branch is not in your codebase. It's drawn to show the shape of an absence, which is the thing prose is worst at describing and the reason this diagram exists at all. The amber box holds one extra condition: ",[44,61,62],{},"AND owner_id = your session user",".",[10,65,66,69,70,74],{},[33,67,68],{},"The two green boxes."," With the condition present, the query returns nothing for a row you don't own, and your handler answers 404. Notice that it's the ",[71,72,73],"em",{},"same"," 404 a genuinely nonexistent invoice would get. That's deliberate. A 403 would confirm the record exists, which hands an attacker a working directory of your customer ids.",[76,77,78],"warning-box",{},[10,79,80,83,84,87],{},[33,81,82],{},"The session user is the part people get wrong."," The condition has to compare against the user id from the server-side session, never against a user id that also came in with the request. An ",[44,85,86],{},"owner_id"," read from a hidden form field, a query parameter, or a JWT claim your app never verified is attacker-controlled too, and the check becomes theatre.",[26,89,91],{"id":90},"why-this-one-shape-covers-so-many-bugs","Why this one shape covers so many bugs",[10,93,94],{},"Swap the nouns and the diagram is unchanged. A document, a team, a project, a support ticket, a webhook endpoint, a file in a bucket. Anywhere your app looks something up by an identifier that came from the request, this same branch is either present or missing.",[10,96,97],{},"That's also why it's worth checking systematically rather than page by page. In scans we see apps where the ownership check is present on four routes and absent on the fifth, usually the one added last, after the pattern had stopped being top of mind.",[26,99,101],{"id":100},"the-source","The source",[103,104,109],"pre",{"className":105,"code":106,"language":107,"meta":108,"style":108},"language-mermaid shiki shiki-themes github-dark","---\ntitle: \"IDOR is one missing condition\"\n---\nflowchart TB\n    REQ[\"GET /api/invoices/1043\u003Cbr/>change 1043 to 1044 and press enter\"]\n    Q[\"The query the AI wrote\u003Cbr/>WHERE id = 1043\"]\n    LEAK[\"Another customer's invoice\u003Cbr/>returned with a 200 OK\"]\n    GATE[\"The condition that is missing\u003Cbr/>\u003Cb>AND owner_id = your session user\u003C/b>\"]\n    SAFE[\"404 Not Found\u003Cbr/>same as a row that never existed\"]\n    OWN[\"Your own invoice\"]\n\n    REQ -->|\"id read straight from the URL\"| Q\n    Q -->|\"what your code actually does\"| LEAK\n    Q -. \"what it never does\" .-> GATE\n    GATE -->|\"row is not yours\"| SAFE\n    GATE -->|\"row is yours\"| OWN\n\n    classDef public fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1c1917\n    classDef gate fill:#fffbeb,stroke:#f59e0b,stroke-width:2px,color:#1c1917\n    classDef private fill:#ecfdf5,stroke:#10b981,stroke-width:2px,color:#1c1917\n    classDef store fill:#f5f5f4,stroke:#57534e,stroke-width:2px,color:#1c1917\n\n    class REQ,LEAK public\n    class GATE gate\n    class SAFE,OWN private\n    class Q store\n\n    linkStyle 1 stroke:#ef4444,stroke-width:3px\n","mermaid","",[44,110,111,119,125,130,136,142,148,154,160,166,172,179,185,191,197,203,209,214,220,226,232,238,243,249,255,261,267,272],{"__ignoreMap":108},[112,113,116],"span",{"class":114,"line":115},"line",1,[112,117,118],{},"---\n",[112,120,122],{"class":114,"line":121},2,[112,123,124],{},"title: \"IDOR is one missing condition\"\n",[112,126,128],{"class":114,"line":127},3,[112,129,118],{},[112,131,133],{"class":114,"line":132},4,[112,134,135],{},"flowchart TB\n",[112,137,139],{"class":114,"line":138},5,[112,140,141],{},"    REQ[\"GET /api/invoices/1043\u003Cbr/>change 1043 to 1044 and press enter\"]\n",[112,143,145],{"class":114,"line":144},6,[112,146,147],{},"    Q[\"The query the AI wrote\u003Cbr/>WHERE id = 1043\"]\n",[112,149,151],{"class":114,"line":150},7,[112,152,153],{},"    LEAK[\"Another customer's invoice\u003Cbr/>returned with a 200 OK\"]\n",[112,155,157],{"class":114,"line":156},8,[112,158,159],{},"    GATE[\"The condition that is missing\u003Cbr/>\u003Cb>AND owner_id = your session user\u003C/b>\"]\n",[112,161,163],{"class":114,"line":162},9,[112,164,165],{},"    SAFE[\"404 Not Found\u003Cbr/>same as a row that never existed\"]\n",[112,167,169],{"class":114,"line":168},10,[112,170,171],{},"    OWN[\"Your own invoice\"]\n",[112,173,175],{"class":114,"line":174},11,[112,176,178],{"emptyLinePlaceholder":177},true,"\n",[112,180,182],{"class":114,"line":181},12,[112,183,184],{},"    REQ -->|\"id read straight from the URL\"| Q\n",[112,186,188],{"class":114,"line":187},13,[112,189,190],{},"    Q -->|\"what your code actually does\"| LEAK\n",[112,192,194],{"class":114,"line":193},14,[112,195,196],{},"    Q -. \"what it never does\" .-> GATE\n",[112,198,200],{"class":114,"line":199},15,[112,201,202],{},"    GATE -->|\"row is not yours\"| SAFE\n",[112,204,206],{"class":114,"line":205},16,[112,207,208],{},"    GATE -->|\"row is yours\"| OWN\n",[112,210,212],{"class":114,"line":211},17,[112,213,178],{"emptyLinePlaceholder":177},[112,215,217],{"class":114,"line":216},18,[112,218,219],{},"    classDef public fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1c1917\n",[112,221,223],{"class":114,"line":222},19,[112,224,225],{},"    classDef gate fill:#fffbeb,stroke:#f59e0b,stroke-width:2px,color:#1c1917\n",[112,227,229],{"class":114,"line":228},20,[112,230,231],{},"    classDef private fill:#ecfdf5,stroke:#10b981,stroke-width:2px,color:#1c1917\n",[112,233,235],{"class":114,"line":234},21,[112,236,237],{},"    classDef store fill:#f5f5f4,stroke:#57534e,stroke-width:2px,color:#1c1917\n",[112,239,241],{"class":114,"line":240},22,[112,242,178],{"emptyLinePlaceholder":177},[112,244,246],{"class":114,"line":245},23,[112,247,248],{},"    class REQ,LEAK public\n",[112,250,252],{"class":114,"line":251},24,[112,253,254],{},"    class GATE gate\n",[112,256,258],{"class":114,"line":257},25,[112,259,260],{},"    class SAFE,OWN private\n",[112,262,264],{"class":114,"line":263},26,[112,265,266],{},"    class Q store\n",[112,268,270],{"class":114,"line":269},27,[112,271,178],{"emptyLinePlaceholder":177},[112,273,275],{"class":114,"line":274},28,[112,276,277],{},"    linkStyle 1 stroke:#ef4444,stroke-width:3px\n",[279,280,281],"faq-section",{},[282,283,285],"faq-item",{"question":284},"What is an IDOR vulnerability?",[10,286,287],{},"Insecure Direct Object Reference. Your app takes an identifier from the request, usually straight out of the URL, looks up that record, and returns it without ever asking whether the record belongs to the person asking. Change 1043 to 1044 in the address bar and you get someone else's data with a normal 200 OK.",[282,289,291],{"question":290},"Why do AI coding tools produce IDOR bugs so reliably?",[10,292,293],{},"Because you asked for a feature, and authorization isn't a feature. A prompt like \"let users view their invoices\" describes the happy path, so the model writes the happy path: find the row, send it back. Nobody wrote down the part where the app refuses. The generated code isn't broken, it's incomplete in a way that runs perfectly in testing.",[282,295,297],{"question":296},"How do I test my own app for IDOR in one minute?",[10,298,299],{},"Sign in as one user, open any page whose URL contains a numeric id, and change the number by one. If you see another account's data, you have an IDOR. Do it again with a second account so you're certain you aren't just looking at your own second record.",[282,301,303],{"question":302},"Should the app return 404 or 403 when the row belongs to someone else?",[10,304,305],{},"404, in nearly every case. A 403 confirms the record exists, which tells an attacker enumerating ids exactly which ones are real. Answering 404 for both \"no such row\" and \"not your row\" gives away nothing.",[282,307,309],{"question":308},"Do random UUIDs fix IDOR?",[10,310,311],{},"No. They make guessing harder, not impossible, and they do nothing once an id leaks through a shared link, a referrer header, an export, or a support ticket. Unguessable identifiers are a speed bump. The ownership condition is the fix.",[10,313,314],{},"::",[316,317,318,323,328],"related-articles",{},[319,320],"related-card",{"description":321,"href":22,"title":322},"The full write-up this diagram belongs to, with the patterns and the fixes in code.","IDOR Explained",[319,324],{"description":325,"href":326,"title":327},"The checkpoint before this one: proving who you are does not prove the row is yours.","/blog/diagrams/auth-vs-authz-checkpoints","Signed In Is Not Allowed",[319,329],{"description":330,"href":331,"title":332},"The wider category IDOR sits in, and the other ways authorization goes missing.","/blog/vulnerabilities/broken-access-control","Broken Access Control",[334,335,338,342],"cta-box",{"href":336,"label":337},"/","Start Free Scan",[26,339,341],{"id":340},"is-the-condition-there-on-every-route","Is the Condition There on Every Route?",[10,343,344],{},"CheckYourVibe probes your deployed API with ids that belong to somebody else and reports the routes that answer with data.",[346,347,348],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":108,"searchDepth":121,"depth":121,"links":350},[351,352,353,354],{"id":28,"depth":121,"text":29},{"id":90,"depth":121,"text":91},{"id":100,"depth":121,"text":101},{"id":340,"depth":121,"text":341},"diagrams","2026-09-09","A flowchart of an IDOR bug. The query the AI wrote looks up the row by id and returns it. The condition that would check whose row it is was never written.",false,"md",[361,363,365,367,369],{"question":284,"answer":362},"Insecure Direct Object Reference. Your app takes an identifier from the request, usually straight out of the URL, looks up that record, and returns it without ever asking whether the record belongs to the person asking. Change 1043 to 1044 in the address bar and you get someone else's data with a normal 200 OK response.",{"question":290,"answer":364},"Because you asked for a feature and authorization is not a feature. A prompt like 'let users view their invoices' describes the happy path, and the model writes the happy path: find the row, send it back. Nobody wrote down the part where the app refuses. The generated code is not broken, it is incomplete in a way that runs perfectly in testing.",{"question":296,"answer":366},"Sign in as one user, open any page whose URL contains a numeric id, and change the number by one. If you see another account's data, you have an IDOR. Do it again with a second account to be certain you are not just looking at your own second record.",{"question":302,"answer":368},"404, in nearly every case. A 403 confirms the record exists, which tells an attacker enumerating ids exactly which ones are real. Answering 404 for both 'no such row' and 'not your row' gives away nothing. The diagram shows this deliberately.",{"question":308,"answer":311},"blue",{"src":372,"alt":19},"https://checkyourvibe.dev/diagrams/idor-the-missing-branch.png","idor diagram, insecure direct object reference, idor vulnerability example, broken object level authorization, ownership check missing, api returns another user's data",{"ogImage":372},"Nothing in the code is wrong. Something in the code is absent. IDOR is a WHERE clause with one condition too few.",null,"/blog/diagrams/idor-the-missing-branch","4 min read","[object Object]","Article",{"title":5,"description":357},{"loc":377},"blog/diagrams/idor-the-missing-branch",[],"summary_large_image","r_-KgkOlkKUY53aLUCBFw9fRb6cmzoe378yHlMrQCDY",1789672845340]