[{"data":1,"prerenderedAt":1575},["ShallowReactive",2],{"announcement":3,"i-tabler:sun":11,"i-tabler:moon":16,"i-tabler:menu-2":18,"i-tabler:x":20,"i-simple-icons:youtube":22,"i-simple-icons:github":24,"i-simple-icons:x":26,"i-simple-icons:linkedin":28,"i-tabler:arrow-up-right":31,"post-race-conditions-are-not-beginner-bugs":33,"post-nav-race-conditions-are-not-beginner-bugs":1560,"video-race-conditions-are-not-beginner-bugs":1566,"i-tabler:copy":1573},{"startsAt":4,"endsAt":5,"scramble":6,"cta":7,"message":10},"2026-11-10T10:00:00-03:00","2026-12-07","Hey, check out the latest article:",{"href":8,"label":9},"\u002Frace-conditions-are-not-beginner-bugs","Read it","Race Conditions Are Not Beginner Bugs.",{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":15},0,24,false,"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 12a4 4 0 1 0 8 0a4 4 0 1 0-8 0m-5 0h1m8-9v1m8 8h1m-9 8v1M5.6 5.6l.7.7m12.1-.7l-.7.7m0 11.4l.7.7m-12.1-.7l-.7.7\"\u002F>",{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":17},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z\"\u002F>",{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":19},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 12h16M4 18h16\"\u002F>",{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":21},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"\u002F>",{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":23},"\u003Cpath fill=\"currentColor\" d=\"M23.498 6.186a3.02 3.02 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.02 3.02 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.02 3.02 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.02 3.02 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814M9.545 15.568V8.432L15.818 12z\"\u002F>",{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":25},"\u003Cpath fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\u002F>",{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":27},"\u003Cpath fill=\"currentColor\" d=\"M14.234 10.162L22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299l-.929-1.329L3.076 1.56h3.182l5.965 8.532l.929 1.329l7.754 11.09h-3.182z\"\u002F>",{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":29,"hidden":30},"\u003Cpath fill=\"currentColor\" d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037c-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85c3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.06 2.06 0 0 1-2.063-2.065a2.064 2.064 0 1 1 2.063 2.065m1.782 13.019H3.555V9h3.564zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0z\"\u002F>",true,{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":32},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M17 7L7 17M8 7h9v9\"\u002F>",{"id":34,"title":35,"body":36,"categories":1538,"cover":1541,"coverImage":1552,"date":4,"description":1553,"draft":14,"extension":1554,"meta":1555,"navigation":30,"noindex":14,"path":8,"readingTime":1556,"seo":1557,"slug":1558,"stem":1558,"updatedAt":1556,"youtubeId":42,"__hash__":1559},"blog\u002Frace-conditions-are-not-beginner-bugs.md","Race Conditions Are Not Beginner Bugs",{"type":37,"value":38,"toc":1520},"minimark",[39,43,47,50,53,61,64,72,75,86,91,94,107,132,135,138,141,144,147,151,154,247,250,253,256,259,262,265,269,272,275,294,300,310,313,363,366,370,373,376,383,390,393,413,416,419,426,429,433,436,439,446,449,452,455,461,464,526,529,532,535,539,542,545,587,590,593,599,602,612,615,667,670,674,677,680,719,722,728,731,734,738,741,748,757,760,771,775,783,789,792,795,800,808,813,821,826,829,832,836,839,842,845,850,857,860,871,874,880,883,886,890,893,902,907,910,913,916,919,922,926,933,936,963,970,973,976,986,989,992,995,1095,1098,1102,1105,1108,1111,1143,1234,1243,1262,1266,1269,1272,1278,1339,1342,1345,1348,1351,1354,1357,1360,1364,1367,1370,1373,1376,1381,1402,1407,1421,1426,1440,1445,1453,1456,1459,1462,1465,1469,1516],[40,41],"post-media",{"video":42},"ugBmwDFi33g",[44,45,46],"p",{},"You've seen a race condition before. Two requests at the same instant, a coupon gets redeemed twice, and somebody ends up with thirty dollars of credit for fifteen.",[44,48,49],{},"Every video on this topic runs that demo. It shoots well. It's also why most of us quietly filed the entire class under beginner and never went back.",[44,51,52],{},"I did that too, for a long time.",[44,54,55,56,60],{},"Then I pointed the exact same technique at a login server. One authorization code, eight requests, all landing in the same millisecond. Eight responses came back. Every one of them a ",[57,58,59],"code",{},"200",", every one carrying a different access token and a different refresh token.",[44,62,63],{},"So I sent that same code one more time, on its own, just to see. And the server told me it had already been used.",[44,65,66,67,71],{},"Read that again, because it's the odd part. Single use ",[68,69,70],"em",{},"is"," enforced on that server. It just isn't enforced during the moment you're racing it.",[44,73,74],{},"Nothing in there is a counter. There's no quota, no ceiling to push through. A login server gave out eight live sessions in exchange for one code, and that doesn't belong on anybody's beginner shelf.",[76,77,83],"sponsor",{"code":78,"cta":79,"href":80,"logo":81,"name":82},"AMRSEC2","Try Cloud Browser","https:\u002F\u002Fbit.ly\u002Fcloud-browser_amrsec","gologin","GoLogin Cloud Browser",[44,84,85],{},"A real Chrome browser running in the cloud. Change one line in your Playwright or Puppeteer script and the profile keeps your cookies, fingerprint and IP together, so the next run starts already logged in. Free to start, no card. The code adds 2 GB of proxy traffic.",[87,88,90],"h2",{"id":89},"only-one-race-category-is-about-limits","Only One Race Category Is About Limits",[44,92,93],{},"Before any of the tooling, I want to deal with the thing that keeps most people out of this class.",[44,95,96,97,106],{},"Go and read ",[98,99,105],"a",{"href":100,"rel":101,"target":104},"https:\u002F\u002Fportswigger.net\u002Fweb-security\u002Frace-conditions?utm_source=amrelsagaei.com&utm_medium=referral",[102,103],"noopener","noreferrer","_blank","PortSwigger's race conditions material"," and count the categories they document:",[108,109,110,114,117,120,123,126,129],"ul",{},[111,112,113],"li",{},"Limit overrun",[111,115,116],{},"Hidden multi-step sequences",[111,118,119],{},"Multi-endpoint race conditions",[111,121,122],{},"Single-endpoint race conditions",[111,124,125],{},"Session-based locking mechanisms",[111,127,128],{},"Partial construction",[111,130,131],{},"Time-sensitive attacks",[44,133,134],{},"Seven of them. One is about limits.",[44,136,137],{},"Nobody in the research ever said go and hunt counters. We did that to ourselves, and it's why the class looks so small from the outside.",[44,139,140],{},"So put the counter down for a minute and think about what a request actually is.",[44,142,143],{},"A request isn't a single event. It's a run of small steps, and the only one that ever reaches you is the last. Between picking your request up and replying, the server moves through internal states, and that stretch is frequently somewhere near a millisecond wide.",[44,145,146],{},"Everything is multi step, even when you only sent one thing.",[87,148,150],{"id":149},"three-lines-of-login-code-are-already-a-race","Three Lines Of Login Code Are Already A Race",[44,152,153],{},"The example that made this click for me is three lines of a login handler.",[155,156,161],"pre",{"code":157,"language":158,"meta":159,"className":160,"style":159},"session['userid'] = user.userid\nif user.mfa_enabled:\n    session['enforce_mfa'] = True\n# then generate the code, send it, redirect\n","python","","shiki shiki-themes vitesse-dark vitesse-light",[57,162,163,201,218,240],{},[164,165,168,172,176,180,184,186,189,192,195,198],"span",{"class":166,"line":167},"line",1,[164,169,171],{"class":170},"sG2Fq","session",[164,173,175],{"class":174},"somML","[",[164,177,179],{"class":178},"sy_C3","'",[164,181,183],{"class":182},"sRRDT","userid",[164,185,179],{"class":178},[164,187,188],{"class":174},"]",[164,190,191],{"class":174}," =",[164,193,194],{"class":170}," user",[164,196,197],{"class":174},".",[164,199,200],{"class":170},"userid\n",[164,202,204,208,210,212,215],{"class":166,"line":203},2,[164,205,207],{"class":206},"sR8n-","if",[164,209,194],{"class":170},[164,211,197],{"class":174},[164,213,214],{"class":170},"mfa_enabled",[164,216,217],{"class":174},":\n",[164,219,221,224,226,228,231,233,235,237],{"class":166,"line":220},3,[164,222,223],{"class":170},"    session",[164,225,175],{"class":174},[164,227,179],{"class":178},[164,229,230],{"class":182},"enforce_mfa",[164,232,179],{"class":178},[164,234,188],{"class":174},[164,236,191],{"class":174},[164,238,239],{"class":206}," True\n",[164,241,243],{"class":166,"line":242},4,[164,244,246],{"class":245},"sy60D","# then generate the code, send it, redirect\n",[44,248,249],{},"Read it and try to find the bad line. There isn't one.",[44,251,252],{},"First line writes your user ID into the session, so you're logged in from that moment. Second line checks whether you've got MFA turned on. Third line sets the flag that enforces it.",[44,254,255],{},"Now sit in the gap between the first line and the third. You're holding a session that's fully logged in and not yet subject to MFA.",[44,257,258],{},"That's what got me the first time I saw it. You can't point at a mistake, because nobody made one. There's an order, and nothing holds that order together.",[44,260,261],{},"The window isn't always that narrow either. Push the second half onto a queue, a background worker or a scheduled job and it stretches to however long that job takes to come round. There are write-ups out there where the two colliding requests were more than twenty minutes apart.",[44,263,264],{},"None of it shows up in the responses. Nothing tells you it's there.",[87,266,268],{"id":267},"three-defences-that-stop-nothing-and-three-that-work","Three Defences That Stop Nothing, And Three That Work",[44,270,271],{},"This next part is where you save yourself real hours, and it's the thing I wish somebody had told me early.",[44,273,274],{},"You're going to read some code, see something that looks like protection, and back off. Three of the usual suspects protect nothing at all.",[44,276,277,281,282,285,286,289,290,293],{},[278,279,280],"strong",{},"A database transaction."," Under InnoDB at ",[57,283,284],{},"REPEATABLE READ",", a plain ",[57,287,288],{},"SELECT"," comes out of a snapshot while an ",[57,291,292],{},"UPDATE"," reads whatever is committed at that second. Both transactions end up on the stale value and both act on it. The transaction is genuine. The protection you assumed it bought you is not.",[44,295,296,299],{},[278,297,298],{},"A lock held in memory."," Fine on one process. Production runs a pool of them, and a lock living inside one worker is invisible to the worker beside it. You will find this handed out as the fix everywhere.",[44,301,302,309],{},[278,303,304,305,308],{},"An ",[57,306,307],{},"Idempotency-Key"," header."," It reads like a stop sign, so go and trace what it does in most codebases. Look the key up, run the action, save the key. Check-then-act wearing a badge, and a minute of testing settles it.",[44,311,312],{},"Now the three that genuinely close the window:",[314,315,316,329],"table",{},[317,318,319],"thead",{},[320,321,322,326],"tr",{},[323,324,325],"th",{},"What it is",[323,327,328],{},"Why it actually holds",[330,331,332,344,355],"tbody",{},[320,333,334,338],{},[335,336,337],"td",{},"A check and an act in one guarded statement",[335,339,340,343],{},[57,341,342],{},"UPDATE codes SET used = true WHERE code = ? AND used = false",", acting only when exactly one row changed. Comparison and write land as a single operation",[320,345,346,349],{},[335,347,348],{},"A row lock with a real write behind it",[335,350,351,354],{},[57,352,353],{},"SELECT ... FOR UPDATE"," and then an actual write. Both halves matter, because a lock with nothing written after it is decoration",[320,356,357,360],{},[335,358,359],{},"A unique constraint on whatever you are duplicating",[335,361,362],{},"Hammer it as hard as you want. The database keeps exactly one",[44,364,365],{},"See any of those three and you can leave. That endpoint is finished, and you just saved yourself an afternoon.",[87,367,369],{"id":368},"requests-have-to-finish-together-not-start-together","Requests Have To Finish Together, Not Start Together",[44,371,372],{},"So if those are the walls, what actually gets you into the gap?",[44,374,375],{},"It comes down to one thing, and it took me far too long to really understand it.",[44,377,378,379,382],{},"A server doesn't start processing your request when the first byte lands. It starts when it decides the request is complete. When the ",[278,380,381],{},"last"," byte lands.",[44,384,385,386,389],{},"That's the hinge, and once you've got it the whole technique makes sense. You're not trying to send eight requests at the same time. You're trying to make eight requests ",[68,387,388],{},"finish"," at the same time, so all of them walk into that one millisecond window together.",[44,391,392],{},"In Caido that lives inside Replay, in a Pipeline session, and there are three strategies:",[108,394,395,401,407],{},[111,396,397,400],{},[278,398,399],{},"Sequential."," One after another, strict order. Everybody clicks straight past this one. Don't, because it's the button that proves your bug is actually a race, and I'll come back to that.",[111,402,403,406],{},[278,404,405],{},"Last Byte Synchronization."," One connection per request, each sent in full except its final byte. Then you fire, and all those final bytes release together.",[111,408,409,412],{},[278,410,411],{},"Single Packet Attack."," Ships greyed out, because it needs HTTP\u002F2. One setting away from working.",[44,414,415],{},"That last one gets explained wrong almost everywhere, so here's what's really happening. You are not sending twenty requests in one packet. Twenty full requests don't fit in one packet. You pre-send the bulk of every request over a single connection, hold back one tiny fragment from each, and then complete twenty to thirty of them with one TCP packet.",[44,417,418],{},"That kills network jitter. Server side jitter is still sitting there, and that's exactly why you queue twenty to thirty instead of two.",[44,420,421,422,425],{},"Fired seventeen thousand kilometres, twenty requests under last byte sync land inside a four millisecond spread. Single packet lands them inside one. Last byte sync does ",[278,423,424],{},"not"," get you to one millisecond, and that claim gets repeated constantly.",[44,427,428],{},"One warning before you flip Caido to HTTP\u002F2. That setting is instance wide. Not per session, not per project. Every tab you've got open and every target in scope switches protocol behaviour at once. On a live engagement you treat that as a decision, not a checkbox.",[87,430,432],{"id":431},"attack-the-server-that-issues-the-token","Attack The Server That Issues The Token",[44,434,435],{},"Now the part where I actually found something.",[44,437,438],{},"An OAuth token finally lands in your hands. What happens next? You start probing what else it reaches. Endpoints that were never in scope, somebody else's records, an ID swapped out to see whether the token still answers.",[44,440,441,442,445],{},"I've done all of that. It isn't wrong. It's just the only move most of us have, and every single one of those tests happens ",[68,443,444],{},"after"," the server already decided to hand you the token.",[44,447,448],{},"So I went the other way. I didn't attack the token, I attacked the thing that hands it out.",[44,450,451],{},"And you get to do that because of two words in the spec. Single use.",[44,453,454],{},"You already know an authorization code is single use. One code, one exchange, then it's spent. Try reading that as a description of server behaviour rather than a property of the code. Single use is a promise, and any promise a server makes is a check executing somewhere in its code.",[155,456,459],{"code":457,"language":458},"read the code   ->   decide it is unused   ->   mark it used\n","text",[57,460,457],{},[44,462,463],{},"If those three aren't one operation, that promise isn't a promise. It's a suggestion. And a suggestion that two requests can both satisfy at the same instant is a race condition.",[155,465,468],{"code":466,"language":467,"meta":159,"className":160,"style":159},"sequenceDiagram\n    participant A as Request A\n    participant S as Token endpoint\n    participant B as Request B\n    A->>S: read code\n    B->>S: read code\n    Note over S: both see \"unused\"\n    S-->>A: mark used, issue tokens\n    S-->>B: mark used, issue tokens\n    Note over A,B: two complete token sets, one code\n","mermaid",[57,469,470,475,480,485,490,496,502,508,514,520],{},[164,471,472],{"class":166,"line":167},[164,473,474],{},"sequenceDiagram\n",[164,476,477],{"class":166,"line":203},[164,478,479],{},"    participant A as Request A\n",[164,481,482],{"class":166,"line":220},[164,483,484],{},"    participant S as Token endpoint\n",[164,486,487],{"class":166,"line":242},[164,488,489],{},"    participant B as Request B\n",[164,491,493],{"class":166,"line":492},5,[164,494,495],{},"    A->>S: read code\n",[164,497,499],{"class":166,"line":498},6,[164,500,501],{},"    B->>S: read code\n",[164,503,505],{"class":166,"line":504},7,[164,506,507],{},"    Note over S: both see \"unused\"\n",[164,509,511],{"class":166,"line":510},8,[164,512,513],{},"    S-->>A: mark used, issue tokens\n",[164,515,517],{"class":166,"line":516},9,[164,518,519],{},"    S-->>B: mark used, issue tokens\n",[164,521,523],{"class":166,"line":522},10,[164,524,525],{},"    Note over A,B: two complete token sets, one code\n",[44,527,528],{},"I want to be honest about how this went, because I wasn't hunting a race condition. I was sitting in front of an OAuth flow asking the one question I ask everywhere. What does this server check before it acts?",[44,530,531],{},"I found it on a real program and I can't name them. What I can share is the scale of it. The program has over eighteen hundred resolved reports behind it. Thousands of people have picked through that app, and this was still sitting in the open, on the token endpoint, right in the middle of the login flow.",[44,533,534],{},"So I built a lab that breaks the same way, and everything below runs on that. You can run it yourself, and honestly I'd rather you did than take my word for it.",[87,536,538],{"id":537},"one-authorization-code-eight-working-token-sets","One Authorization Code, Eight Working Token Sets",[44,540,541],{},"The thing I like about this request is that I'm not supposed to see it.",[44,543,544],{},"When the app trades your authorization code for tokens, that happens server to server, in the background. The only reason it's sitting in front of me is that I'm the one running the app. Catch it on the way out and the body has everything in it:",[155,546,549],{"code":547,"language":548,"meta":159,"className":160,"style":159},"POST \u002Foauth2\u002Ftoken HTTP\u002F1.1\nContent-Type: application\u002Fx-www-form-urlencoded\n\ngrant_type=authorization_code&code=...&redirect_uri=...&client_id=...&client_secret=...&code_verifier=...\n","http",[57,550,551,569,577,582],{},[164,552,553,556,559,562,565],{"class":166,"line":167},[164,554,555],{"class":206},"POST",[164,557,558],{"class":170}," \u002Foauth2\u002Ftoken ",[164,560,561],{"class":206},"HTTP",[164,563,564],{"class":170},"\u002F",[164,566,568],{"class":567},"sUQhE","1.1\n",[164,570,571,574],{"class":166,"line":203},[164,572,573],{"class":206},"Content-Type:",[164,575,576],{"class":182}," application\u002Fx-www-form-urlencoded\n",[164,578,579],{"class":166,"line":220},[164,580,581],{"emptyLinePlaceholder":30},"\n",[164,583,584],{"class":166,"line":242},[164,585,586],{"class":170},"grant_type=authorization_code&code=...&redirect_uri=...&client_id=...&client_secret=...&code_verifier=...\n",[44,588,589],{},"So I dropped it. The app never received its tokens and visibly fell over behind me, which is exactly what should happen. The code itself though was never redeemed. Still live, and now mine.",[44,591,592],{},"Eight entries, all carrying that one code, strategy set to last byte synchronisation. Then run.",[44,594,595,596,598],{},"Every status came back ",[57,597,59],{},". Eight of them. And when I opened them up, each one held a different access token and a different refresh token.",[44,600,601],{},"Now eight different values only prove one thing, and it's worth being strict about this. They prove the server issued eight times. They don't prove a single one of them works.",[44,603,604,605,608,609,611],{},"So I took two of them, and not the first one. Two from the middle of the batch. Each one went to ",[57,606,607],{},"\u002Foauth2\u002Fuserinfo"," on its own, with nothing else running. Both came back ",[57,610,59],{}," with the account in the body.",[44,613,614],{},"Then I went back to the single request I started with, put that same already-used code in it, and sent it on its own:",[155,616,618],{"code":617,"language":548,"meta":159,"className":160,"style":159},"HTTP\u002F1.1 401 Unauthorized\n\n{\"error\":\"invalid_verification\"}\n",[57,619,620,635,639],{},[164,621,622,624,626,629,632],{"class":166,"line":167},[164,623,561],{"class":206},[164,625,564],{"class":170},[164,627,628],{"class":567},"1.1",[164,630,631],{"class":567}," 401",[164,633,634],{"class":182}," Unauthorized\n",[164,636,637],{"class":166,"line":203},[164,638,581],{"emptyLinePlaceholder":30},[164,640,641,644,648,652,654,657,659,662,664],{"class":166,"line":220},[164,642,643],{"class":174},"{",[164,645,647],{"class":646},"sIkdJ","\"",[164,649,651],{"class":650},"s6_DI","error",[164,653,647],{"class":646},[164,655,656],{"class":174},":",[164,658,647],{"class":178},[164,660,661],{"class":182},"invalid_verification",[164,663,647],{"class":178},[164,665,666],{"class":174},"}\n",[44,668,669],{},"Which tells you the server is fully aware. The check runs. It just wasn't running in the moment eight requests arrived at once.",[87,671,673],{"id":672},"the-sequential-run-is-what-proves-it-is-a-race","The Sequential Run Is What Proves It Is A Race",[44,675,676],{},"And that brings me back to the button I told you not to skip.",[44,678,679],{},"Fresh code, same eight requests, and this time the strategy is Sequential. One request, wait for it to finish, then the next. The exact opposite of what I want, which is the whole point.",[314,681,682,695],{},[317,683,684],{},[320,685,686,689,692],{},[323,687,688],{},"Strategy",[323,690,691],{},"Accepted",[323,693,694],{},"Rejected",[330,696,697,708],{},[320,698,699,702,705],{},[335,700,701],{},"Sequential",[335,703,704],{},"1",[335,706,707],{},"7",[320,709,710,713,716],{},[335,711,712],{},"Last Byte Synchronization",[335,714,715],{},"8",[335,717,718],{},"0",[44,720,721],{},"Those two rows have to be read side by side, because this is what your whole report rests on.",[44,723,724,725,727],{},"Had the sequential run returned eight ",[57,726,59],{},"s, no check would exist anywhere on that server. Nothing would ever consult whether the code had been spent, and what you'd have is a logic flaw. Real enough, but it needs a different title, a different severity and a different fix.",[44,729,730],{},"That isn't what came back. Sequential returns one success and seven refusals, which proves the check is alive and doing its job. The server is perfectly capable of saying no. Parallel returns eight, which proves that same check isn't atomic.",[44,732,733],{},"Write those two sentences into your report and you have a race condition on paper. Leave them out and all a triager sees is eight tokens, with nothing telling them whether that server validates anything at all. Informative, closed.",[87,735,737],{"id":736},"why-pkce-does-not-stop-any-of-this","Why PKCE Does Not Stop Any Of This",[44,739,740],{},"Two things people throw at me when I describe this, and neither one holds up.",[44,742,743,744,747],{},"The first is PKCE. PKCE was built for a stolen code, and I didn't steal anything. I'm the client. I started this flow, and I'm holding the ",[57,745,746],{},"code_verifier"," myself.",[44,749,750,751,756],{},"Then look at what the check actually is. ",[98,752,755],{"href":753,"rel":754,"target":104},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc7636.html?utm_source=amrelsagaei.com&utm_medium=referral",[102,103],"RFC 7636"," defines it as a value comparison. You compare what came in against what was stored, and you move on. Nothing gets consumed, and no state changes anywhere.",[44,758,759],{},"Ask a comparison the same question eight times and it answers identically eight times. That is the entire nature of a comparison.",[44,761,762,763,766,767,770],{},"Search that RFC for race, concurrent, replay or single use and you get nothing back. Not one hit. PKCE changes ",[68,764,765],{},"who"," can redeem a stolen code. It doesn't change ",[68,768,769],{},"how many times"," the holder can.",[87,772,774],{"id":773},"which-rfc-line-to-quote-and-which-one-gets-you-closed","Which RFC Line To Quote, And Which One Gets You Closed",[44,776,777,778,656],{},"The second thing is the line everybody pastes into the report, ",[98,779,782],{"href":780,"rel":781,"target":104},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6749?utm_source=amrelsagaei.com&utm_medium=referral#section-4.1.2",[102,103],"RFC 6749 §4.1.2",[784,785,786],"blockquote",{},[44,787,788],{},"The client MUST NOT use the authorization code more than once.",[44,790,791],{},"Check the subject of that sentence. It's the client. The obligation lands on the app requesting tokens, not on the server issuing them. Paste it into a report and a triager points out that nobody instructed the server to do anything. They'll be correct, and you'll be closed.",[44,793,794],{},"Three other lines do the work, and the first one is sitting right there in the same paragraph:",[784,796,797],{},[44,798,799],{},"If an authorization code is used more than once, the authorization server MUST deny the request and SHOULD revoke (when possible) all tokens previously issued based on that authorization code.",[44,801,802,803,656],{},"Then ",[98,804,807],{"href":805,"rel":806,"target":104},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6749?utm_source=amrelsagaei.com&utm_medium=referral#section-10.5",[102,103],"RFC 6749 §10.5",[784,809,810],{},[44,811,812],{},"Authorization codes MUST be short lived and single-use.",[44,814,815,816,656],{},"And ",[98,817,820],{"href":818,"rel":819,"target":104},"https:\u002F\u002Fwww.ietf.org\u002Farchive\u002Fid\u002Fdraft-ietf-oauth-v2-1-15.html?utm_source=amrelsagaei.com&utm_medium=referral",[102,103],"OAuth 2.1, draft 15, §4.1.3",[784,822,823],{},[44,824,825],{},"The authorization server MUST return an access token only once for a given authorization code.",[44,827,828],{},"Only once, for a given code, and this time the sentence is pointed squarely at the server.",[44,830,831],{},"Identical finding either way. Identical eight tokens. What decides whether it gets triaged or closed is which sentence you chose to paste.",[87,833,835],{"id":834},"report-the-detection-bypass-not-the-token-count","Report The Detection Bypass, Not The Token Count",[44,837,838],{},"Here's where I nearly wrote a bad report.",[44,840,841],{},"Lead with \"one code gave me eight access tokens\" and the report is sunk before anyone reaches paragraph two. Eight of anything looks like limit overrun, and limit overrun gets rated Low. Usually fairly.",[44,843,844],{},"So I went back to why single use exists in the first place. The stock answer is that it stops a code being redeemed twice, and that covers maybe half of it.",[44,846,847],{},[278,848,849],{},"Single use is a detection mechanism.",[44,851,852,853,856],{},"Picture how it's meant to play out. A code leaks somewhere it shouldn't. Whoever picks it up cashes it in ahead of the real client and walks off with a token. The real client then turns up with the same code, gets ",[57,854,855],{},"invalid_grant"," back, and the login visibly falls over for the person sitting in front of it.",[44,858,859],{},"That failure is the entire design. It doesn't just break, it breaks noisily:",[108,861,862,865,868],{},[111,863,864],{},"The user sees a broken login and either retries it or complains about it.",[111,866,867],{},"The integrator sees a failed exchange sitting in their logs.",[111,869,870],{},"Somebody finally has a reason to investigate.",[44,872,873],{},"Drop the race on top and both exchanges go through. The attacker ends up holding a live token. The victim lands on their dashboard with a perfectly healthy session, nothing to retry and nothing worth mentioning to anyone. No error surfaces, no exchange fails, and not one log line anywhere records a code being spent twice. By that system's own books, it never was.",[44,875,876,877],{},"The advisory names that impact in two words. ",[278,878,879],{},"Detection bypass.",[44,881,882],{},"And that flips the whole report. The finding isn't that eight tokens exist. The finding is that the one alarm capable of telling them a code had been stolen is now switched off.",[44,884,885],{},"Severity isn't measured by what I walked away with. It's measured by what they lost the ability to see. Hand a triager a number and they'll happily count it down to Low alongside you. Hand them a missing control and there's nothing left to count. It's either running or it isn't.",[87,887,889],{"id":888},"never-claim-independence-you-have-not-tested","Never Claim Independence You Have Not Tested",[44,891,892],{},"There's a sentence I nearly put in that report, and I'm glad I didn't.",[44,894,895,896,901],{},"It's tempting to write that eight different refresh tokens prove eight independent grants. They don't. ",[98,897,900],{"href":898,"rel":899,"target":104},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6749?utm_source=amrelsagaei.com&utm_medium=referral#section-6",[102,103],"RFC 6749 §6"," lets an authorization server keep several refresh tokens alive under one grant:",[784,903,904],{},[44,905,906],{},"The authorization server MAY revoke the old refresh token after issuing a new refresh token to the client.",[44,908,909],{},"May. So it is equally free not to.",[44,911,912],{},"Independence, then, is something a server does, not something a token string tells you. You prove it by testing, which is precisely why two of those tokens went out alone before I typed a line of the write-up.",[44,914,915],{},"Any triager who has read that section removes the claim in a single reply. From there, every other sentence you wrote is being read by somebody who already caught you stretching once. Stick to what you watched happen, and never hand over a conclusion that can be taken apart.",[44,917,918],{},"The same discipline applies to me. I went and tested revocation on that program deliberately. Killed the application grant, waited, and inside about forty five seconds all eight were dead. There's no persistence angle on that target, so I won't pretend there is.",[44,920,921],{},"It did leave me with a question I ran out of room to answer. If independence only counts when you test it, I needed a target where I could test it to the end. Somewhere I could revoke things, tear consent out, sign accounts out, and keep going until something finally broke.",[87,923,925],{"id":924},"the-two-lines-of-library-code-behind-it","The Two Lines Of Library Code Behind It",[44,927,928,929,932],{},"Before pulling it apart, I went looking for ",[68,930,931],{},"why",", because this didn't feel like one app being sloppy.",[44,934,935],{},"It isn't. The same mistake ships inside a library, and it has a CVE on it.",[44,937,938,943,944,947,948,951,952,955,956,959,960,197],{},[98,939,942],{"href":940,"rel":941,"target":104},"https:\u002F\u002Fgithub.com\u002Fadvisories\u002FGHSA-7w99-5wm4-3g79?utm_source=amrelsagaei.com&utm_medium=referral",[102,103],"CVE-2026-53518",", rated High, published May 2026. GitHub scores it ",[278,945,946],{},"7.6"," on CVSS v4.0, and it carries ",[278,949,950],{},"8.1"," on v3.1. It covers ",[57,953,954],{},"@better-auth\u002Foauth-provider"," from 1.6.0 up to 1.6.11, and ",[57,957,958],{},"better-auth"," itself below 1.6.11 through the legacy provider paths. The advisory title describes it better than I can: ",[68,961,962],{},"concurrent redemption when two token requests race the find-then-delete primitive",[44,964,965,966,969],{},"Open up ",[57,967,968],{},"checkVerificationValue"," and there are two calls, one after the other. The first reads the row for that code. The second deletes it. Two awaits with a gap between them, and that gap is where every single thing above happens.",[44,971,972],{},"Two requests turn up together. Neither delete has executed yet, so the read waves both of them straight through, and both carry on to mint a full set of tokens.",[44,974,975],{},"And here's the detail that made me sit back.",[44,977,978,979,982,983,197],{},"Look at what that delete hands back. Nothing. There's no ",[57,980,981],{},"return"," statement in it. It removes the row and resolves to ",[57,984,985],{},"undefined",[44,987,988],{},"The database counted the rows it just removed. One, or zero. That count answers the only question in play: did this request get the code, or was somebody here first? The function bins the number, so nothing upstream can ever learn that it lost. No exception, no branch to take, nothing available to inspect, because nothing was handed back.",[44,990,991],{},"A guarded write reports its row count and you continue only when it says one. This one deletes and shrugs.",[44,993,994],{},"The fix is exactly what you'd expect:",[996,997,999,1069],"code-group",{"label":998},"Build",[155,1000,1004],{"code":1001,"filename":1002,"language":1003,"meta":159,"className":160,"style":159},"const verification = await findVerificationValue(...)\nif (!verification) throw new APIError(...)\nawait deleteVerificationByIdentifier(...)   \u002F\u002F resolves to undefined\n","1.6.10, vulnerable","js",[57,1005,1006,1028,1055],{},[164,1007,1008,1012,1016,1018,1021,1025],{"class":166,"line":167},[164,1009,1011],{"class":1010},"sfIIG","const",[164,1013,1015],{"class":1014},"snInM"," verification",[164,1017,191],{"class":174},[164,1019,1020],{"class":206}," await",[164,1022,1024],{"class":1023},"swzfi"," findVerificationValue",[164,1026,1027],{"class":174},"(...)\n",[164,1029,1030,1032,1035,1038,1041,1044,1047,1050,1053],{"class":166,"line":203},[164,1031,207],{"class":206},[164,1033,1034],{"class":174}," (",[164,1036,1037],{"class":1010},"!",[164,1039,1040],{"class":1014},"verification",[164,1042,1043],{"class":174},")",[164,1045,1046],{"class":206}," throw",[164,1048,1049],{"class":1010}," new",[164,1051,1052],{"class":1023}," APIError",[164,1054,1027],{"class":174},[164,1056,1057,1060,1063,1066],{"class":166,"line":220},[164,1058,1059],{"class":206},"await",[164,1061,1062],{"class":1023}," deleteVerificationByIdentifier",[164,1064,1065],{"class":174},"(...)",[164,1067,1068],{"class":245},"   \u002F\u002F resolves to undefined\n",[155,1070,1073],{"code":1071,"filename":1072,"language":1003,"meta":159,"className":160,"style":159},"const verification = await consumeVerificationValue(...)\n\u002F\u002F deletes the row and hands back what it held, in one operation\n","1.6.11, patched",[57,1074,1075,1090],{},[164,1076,1077,1079,1081,1083,1085,1088],{"class":166,"line":167},[164,1078,1011],{"class":1010},[164,1080,1015],{"class":1014},[164,1082,191],{"class":174},[164,1084,1020],{"class":206},[164,1086,1087],{"class":1023}," consumeVerificationValue",[164,1089,1027],{"class":174},[164,1091,1092],{"class":166,"line":203},[164,1093,1094],{"class":245},"\u002F\u002F deletes the row and hands back what it held, in one operation\n",[44,1096,1097],{},"A value comes back and the code was yours. Nothing comes back and somebody beat you to it. Two calls collapsed into one, and that is the entire patch.",[87,1099,1101],{"id":1100},"fingerprint-the-vulnerable-build-with-one-request","Fingerprint The Vulnerable Build With One Request",[44,1103,1104],{},"That difference handed me something I use constantly now, and it's the part you can take to a target today.",[44,1106,1107],{},"Telling the two builds apart needs no race at all, and no account either.",[44,1109,1110],{},"Post a garbage code to the token endpoint. None of it has to be real, and you never send a client secret:",[155,1112,1114],{"code":1113,"language":548,"meta":159,"className":160,"style":159},"POST \u002Foauth2\u002Ftoken HTTP\u002F1.1\nContent-Type: application\u002Fx-www-form-urlencoded\n\ngrant_type=authorization_code&code=aaaaaaaa&code_verifier=aaaaaaaa&client_id=zzzzzzzz&redirect_uri=https%3A%2F%2Fexample.com%2Fcb\n",[57,1115,1116,1128,1134,1138],{},[164,1117,1118,1120,1122,1124,1126],{"class":166,"line":167},[164,1119,555],{"class":206},[164,1121,558],{"class":170},[164,1123,561],{"class":206},[164,1125,564],{"class":170},[164,1127,568],{"class":567},[164,1129,1130,1132],{"class":166,"line":203},[164,1131,573],{"class":206},[164,1133,576],{"class":182},[164,1135,1136],{"class":166,"line":220},[164,1137,581],{"emptyLinePlaceholder":30},[164,1139,1140],{"class":166,"line":242},[164,1141,1142],{"class":170},"grant_type=authorization_code&code=aaaaaaaa&code_verifier=aaaaaaaa&client_id=zzzzzzzz&redirect_uri=https%3A%2F%2Fexample.com%2Fcb\n",[996,1144,1146,1192],{"label":1145},"Response",[155,1147,1151],{"code":1148,"filename":1149,"language":1150,"meta":159,"className":160,"style":159},"{\"error_description\":\"Invalid code\",\"error\":\"invalid_verification\"}\n","Vulnerable, below 1.6.11","json",[57,1152,1153],{},[164,1154,1155,1157,1159,1162,1164,1166,1168,1171,1173,1176,1178,1180,1182,1184,1186,1188,1190],{"class":166,"line":167},[164,1156,643],{"class":174},[164,1158,647],{"class":646},[164,1160,1161],{"class":650},"error_description",[164,1163,647],{"class":646},[164,1165,656],{"class":174},[164,1167,647],{"class":178},[164,1169,1170],{"class":182},"Invalid code",[164,1172,647],{"class":178},[164,1174,1175],{"class":174},",",[164,1177,647],{"class":646},[164,1179,651],{"class":650},[164,1181,647],{"class":646},[164,1183,656],{"class":174},[164,1185,647],{"class":178},[164,1187,661],{"class":182},[164,1189,647],{"class":178},[164,1191,666],{"class":174},[155,1193,1196],{"code":1194,"filename":1195,"language":1150,"meta":159,"className":160,"style":159},"{\"error_description\":\"Invalid code\",\"error\":\"invalid_grant\"}\n","Patched, 1.6.11 and later",[57,1197,1198],{},[164,1199,1200,1202,1204,1206,1208,1210,1212,1214,1216,1218,1220,1222,1224,1226,1228,1230,1232],{"class":166,"line":167},[164,1201,643],{"class":174},[164,1203,647],{"class":646},[164,1205,1161],{"class":650},[164,1207,647],{"class":646},[164,1209,656],{"class":174},[164,1211,647],{"class":178},[164,1213,1170],{"class":182},[164,1215,647],{"class":178},[164,1217,1175],{"class":174},[164,1219,647],{"class":646},[164,1221,651],{"class":650},[164,1223,647],{"class":646},[164,1225,656],{"class":174},[164,1227,647],{"class":178},[164,1229,855],{"class":182},[164,1231,647],{"class":178},[164,1233,666],{"class":174},[44,1235,1236,1237,1239,1240,1242],{},"The old build answers ",[57,1238,661],{},". The patched one answers ",[57,1241,855],{},". No login, no valid code, nothing, and a single request tells you whether that host is patched before you spend any real time on it.",[44,1244,1245,1246,1249,1250,1253,1254,1257,1258,1261],{},"One thing that'll catch you out. All four fields have to be there or an earlier check returns a ",[57,1247,1248],{},"400"," and you never reach the code lookup. None of them has to be real, though. A made up ",[57,1251,1252],{},"client_id"," and a ",[57,1255,1256],{},"redirect_uri"," pointing at ",[57,1259,1260],{},"example.com"," both give you the answer.",[87,1263,1265],{"id":1264},"the-revoke-button-that-revokes-nothing","The Revoke Button That Revokes Nothing",[44,1267,1268],{},"So back to the question I couldn't finish on the real program. On my own lab there's no scope page telling me where to stop, so I kept going.",[44,1270,1271],{},"Twenty four sockets, one authorization code, twenty four independent token sets. Every socket won.",[44,1273,1274,1275,1277],{},"I took one refresh token and hit the revoke endpoint. ",[57,1276,59],{}," back, clean response, and exactly one branch died.",[155,1279,1281],{"code":1280,"language":467,"meta":159,"className":160,"style":159},"flowchart TD\n    C[\"One authorization code\"] --> F[\"24 token sets\"]\n    F --> R[\"Revoke one refresh token: 200\"]\n    R --> D[\"1 branch dies\"]\n    R --> S[\"23 keep serving userinfo\"]\n    S --> A[\"Remove app from Connected apps\"]\n    A --> S2[\"23 still alive\"]\n    S2 --> O[\"Sign the user out\"]\n    O --> S3[\"23 still alive, still rotating\"]\n    S3 --> P[\"Replay the revoked token\"]\n    P --> K[\"Reuse detection fires, all 23 die\"]\n",[57,1282,1283,1288,1293,1298,1303,1308,1313,1318,1323,1328,1333],{},[164,1284,1285],{"class":166,"line":167},[164,1286,1287],{},"flowchart TD\n",[164,1289,1290],{"class":166,"line":203},[164,1291,1292],{},"    C[\"One authorization code\"] --> F[\"24 token sets\"]\n",[164,1294,1295],{"class":166,"line":220},[164,1296,1297],{},"    F --> R[\"Revoke one refresh token: 200\"]\n",[164,1299,1300],{"class":166,"line":242},[164,1301,1302],{},"    R --> D[\"1 branch dies\"]\n",[164,1304,1305],{"class":166,"line":492},[164,1306,1307],{},"    R --> S[\"23 keep serving userinfo\"]\n",[164,1309,1310],{"class":166,"line":498},[164,1311,1312],{},"    S --> A[\"Remove app from Connected apps\"]\n",[164,1314,1315],{"class":166,"line":504},[164,1316,1317],{},"    A --> S2[\"23 still alive\"]\n",[164,1319,1320],{"class":166,"line":510},[164,1321,1322],{},"    S2 --> O[\"Sign the user out\"]\n",[164,1324,1325],{"class":166,"line":516},[164,1326,1327],{},"    O --> S3[\"23 still alive, still rotating\"]\n",[164,1329,1330],{"class":166,"line":522},[164,1331,1332],{},"    S3 --> P[\"Replay the revoked token\"]\n",[164,1334,1336],{"class":166,"line":1335},11,[164,1337,1338],{},"    P --> K[\"Reuse detection fires, all 23 die\"]\n",[44,1340,1341],{},"The other twenty three kept working. Still serving user info, still rotating refresh tokens, three rounds later.",[44,1343,1344],{},"So I pulled the entire app out of Connected apps. Nothing changed. Twenty three still alive. Then I signed the account out of the workspace completely. Still twenty three.",[44,1346,1347],{},"One action finally took them down, and it was presenting the revoked token a second time. Which happens to be the exact thing nobody does when they're trying to shut access off. Present it again and the server reads it as theft. Reuse detection kicks in and clears every token belonging to that app and that user. Twenty three dead in one go.",[44,1349,1350],{},"So the logic capable of ending all of them shipped with the product. It simply never fires from the button labelled revoke.",[44,1352,1353],{},"Sit with how that reads to the account holder. Settings page, connected app in the list, click revoke, green checkmark, done.",[44,1355,1356],{},"Behind that checkmark, twenty three sessions nobody can see are still running.",[44,1358,1359],{},"There's no money in this one. The damage is that revocation doesn't revoke, and not one person involved ever finds out.",[87,1361,1363],{"id":1362},"eight-questions-to-ask-at-any-endpoint","Eight Questions To Ask At Any Endpoint",[44,1365,1366],{},"I want to leave you with the thing that actually found this, because it wasn't a race condition technique.",[44,1368,1369],{},"HTML injection isn't obliged to stay HTML injection. That label stuck because it was the only word you had the second a tag rendered. It could be a template engine. It could be two parsers disagreeing about one string.",[44,1371,1372],{},"Whatever you called it in minute one quietly decided your next two hours. Which files you opened, which payloads you reached for, the point where you gave up. Nothing about the application chose that label. You did.",[44,1374,1375],{},"So here's what I ask at an endpoint instead of reaching for a class name.",[44,1377,1378],{},[278,1379,1380],{},"Three that kill most endpoints in about a minute:",[1382,1383,1384,1390,1396],"ol",{},[111,1385,1386,1389],{},[278,1387,1388],{},"What does it read before it writes anything?"," A balance. A used flag. A seat count, an email binding, a role, a filename. If no answer comes, there's nothing here to race and you can move on.",[111,1391,1392,1395],{},[278,1393,1394],{},"Do the read and the write hit the same record, under the same key?"," If they don't, there's no collision to be had.",[111,1397,1398,1401],{},[278,1399,1400],{},"Is that guard the only one?"," When a single boolean carries the whole security model, race it.",[44,1403,1404],{},[278,1405,1406],{},"Two that find what everybody else walks past:",[1382,1408,1409,1415],{"start":242},[111,1410,1411,1414],{},[278,1412,1413],{},"Who else writes to that record?"," Put every endpoint that touches it on a list. That list is where multi-endpoint races come from, and where the serious impact sits.",[111,1416,1417,1420],{},[278,1418,1419],{},"Is this request doing two jobs?"," Two rows written. An email sent and a token stored. A session established, then a flag raised. That's the login handler from earlier in a costume.",[44,1422,1423],{},[278,1424,1425],{},"Two that change where you're looking:",[1382,1427,1428,1434],{"start":498},[111,1429,1430,1433],{},[278,1431,1432],{},"Has any of the work been deferred?"," A queue, a worker, a cron, a webhook. You already know what each of those does to the size of the window.",[111,1435,1436,1439],{},[278,1437,1438],{},"Does the output land somewhere shared and keyed?"," A cache, a CDN, a temp path. If so it stops being a race and becomes poisoning, and it reaches people who never went near your account.",[44,1441,1442],{},[278,1443,1444],{},"One that writes your report:",[1382,1446,1447],{"start":510},[111,1448,1449,1452],{},[278,1450,1451],{},"What breaks if that guard never runs at all?"," Not once instead of twice. Zero. Whatever you answer there is your impact section, already written for you.",[44,1454,1455],{},"Two of those carry the weight. Four is what separates a single endpoint from a chain. Eight is what separates a fifty dollar duplicate from an account takeover.",[44,1457,1458],{},"Almost everyone asks number one and stops there, and only ever on counters.",[44,1460,1461],{},"I assumed at first that this belonged to that one program. It didn't. Same requests, same setup, and it kept landing elsewhere. Most of them I still haven't touched.",[44,1463,1464],{},"I'm not claiming anything by that. Take it as an invitation.",[87,1466,1468],{"id":1467},"further-reading","Further Reading",[108,1470,1471,1477,1483,1490,1497,1503,1509],{},[111,1472,1473],{},[98,1474,1476],{"href":940,"rel":1475,"target":104},[102,103],"CVE-2026-53518 advisory, GHSA-7w99-5wm4-3g79",[111,1478,1479],{},[98,1480,1482],{"href":100,"rel":1481,"target":104},[102,103],"PortSwigger, Race conditions",[111,1484,1485],{},[98,1486,1489],{"href":1487,"rel":1488,"target":104},"https:\u002F\u002Fportswigger.net\u002Fresearch\u002Fsmashing-the-state-machine?utm_source=amrelsagaei.com&utm_medium=referral",[102,103],"James Kettle, Smashing the state machine",[111,1491,1492],{},[98,1493,1496],{"href":1494,"rel":1495,"target":104},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6749?utm_source=amrelsagaei.com&utm_medium=referral",[102,103],"RFC 6749, The OAuth 2.0 Authorization Framework",[111,1498,1499],{},[98,1500,1502],{"href":753,"rel":1501,"target":104},[102,103],"RFC 7636, Proof Key for Code Exchange",[111,1504,1505],{},[98,1506,1508],{"href":818,"rel":1507,"target":104},[102,103],"OAuth 2.1, draft 15",[111,1510,1511],{},[98,1512,1515],{"href":1513,"rel":1514,"target":104},"https:\u002F\u002Fdocs.caido.io\u002F?utm_source=amrelsagaei.com&utm_medium=referral",[102,103],"Caido documentation",[1517,1518,1519],"style",{},"html pre.shiki code .sG2Fq, html code.shiki .sG2Fq{--shiki-default:#DBD7CAEE;--shiki-light:#393A34}html pre.shiki code .somML, html code.shiki .somML{--shiki-default:#666666;--shiki-light:#999999}html pre.shiki code .sy_C3, html code.shiki .sy_C3{--shiki-default:#C98A7D77;--shiki-light:#B5695977}html pre.shiki code .sRRDT, html code.shiki .sRRDT{--shiki-default:#C98A7D;--shiki-light:#B56959}html pre.shiki code .sR8n-, html code.shiki .sR8n-{--shiki-default:#4D9375;--shiki-light:#1E754F}html pre.shiki code .sy60D, html code.shiki .sy60D{--shiki-default:#758575DD;--shiki-light:#A0ADA0}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);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sUQhE, html code.shiki .sUQhE{--shiki-default:#4C9A91;--shiki-light:#2F798A}html pre.shiki code .sIkdJ, html code.shiki .sIkdJ{--shiki-default:#B8A96577;--shiki-light:#99841877}html pre.shiki code .s6_DI, html code.shiki .s6_DI{--shiki-default:#B8A965;--shiki-light:#998418}html pre.shiki code .sfIIG, html code.shiki .sfIIG{--shiki-default:#CB7676;--shiki-light:#AB5959}html pre.shiki code .snInM, html code.shiki .snInM{--shiki-default:#BD976A;--shiki-light:#B07D48}html pre.shiki code .swzfi, html code.shiki .swzfi{--shiki-default:#80A665;--shiki-light:#59873A}",{"title":159,"searchDepth":220,"depth":220,"links":1521},[1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537],{"id":89,"depth":203,"text":90},{"id":149,"depth":203,"text":150},{"id":267,"depth":203,"text":268},{"id":368,"depth":203,"text":369},{"id":431,"depth":203,"text":432},{"id":537,"depth":203,"text":538},{"id":672,"depth":203,"text":673},{"id":736,"depth":203,"text":737},{"id":773,"depth":203,"text":774},{"id":834,"depth":203,"text":835},{"id":888,"depth":203,"text":889},{"id":924,"depth":203,"text":925},{"id":1100,"depth":203,"text":1101},{"id":1264,"depth":203,"text":1265},{"id":1362,"depth":203,"text":1363},{"id":1467,"depth":203,"text":1468},[1539,1540],"Bug Bounty","Penetration Testing",{"width":1542,"height":1543,"variants":1544,"lqip":1547,"social":1548},1920,1080,{"400":1545,"800":1546},"\u002Fimages\u002Fblog\u002Frace-conditions-are-not-beginner-bugs\u002Frace-conditions-are-not-beginner-bugs-400.webp","\u002Fimages\u002Fblog\u002Frace-conditions-are-not-beginner-bugs\u002Frace-conditions-are-not-beginner-bugs-800.webp","data:image\u002Fwebp;base64,UklGRmIAAABXRUJQVlA4IFYAAADQAwCdASoUAAsAPu1iqk2ppaQiMAgBMB2JYgCsABu1QZs8\u002F1fonrAA\u002Fu9jItEXRwatfHl+xoOE2Pi\u002F6X1t63MKlN6ApYerCaayv2fMzJDgYLdz13AAAA==",{"src":1549,"width":1550,"height":1551},"\u002Fimages\u002Fblog\u002Frace-conditions-are-not-beginner-bugs\u002Frace-conditions-are-not-beginner-bugs-og.jpg",1200,675,"\u002Fimages\u002Fblog\u002Frace-conditions-are-not-beginner-bugs\u002Frace-conditions-are-not-beginner-bugs.webp","One authorization code, eight working token sets, and a revoke button that does nothing. Race conditions stop being a coupon demo when you point them at auth.","md",{},null,{"title":35,"description":1553},"race-conditions-are-not-beginner-bugs","8QMvB6C7ks_YpnuP-Axed-waEEaMWPuqklAfu42j4xY",{"older":1561,"newer":1556,"total":1565},{"slug":1562,"title":1563,"date":1564,"position":203},"duplicates-are-part-of-the-process","Duplicates Are Part Of The Process","2026-10-20T13:00:00Z",70,{"id":42,"title":35,"published":1567,"url":1568,"description":1569,"durationSeconds":1570,"thumbnail":1571,"thumbnailSrcset":1572},"2026-11-10T13:00:34+00:00","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=ugBmwDFi33g","One authorization code, eight requests completing in the same millisecond, and eight working token sets come back. Send that same code once more on its own and the server tells you it has already been used, so the check exists and just is not atomic.",2745,"\u002Fimages\u002Fvideos\u002FugBmwDFi33g-400.webp","\u002Fimages\u002Fvideos\u002FugBmwDFi33g-400.webp 400w, \u002Fimages\u002Fvideos\u002FugBmwDFi33g-800.webp 800w, \u002Fimages\u002Fvideos\u002FugBmwDFi33g-1280.webp 1280w",{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":1574},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z\"\u002F>\u003Cpath d=\"M4.012 16.737A2 2 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1\"\u002F>\u003C\u002Fg>",1794315888776]