The Last Proof of Human Craft: Forging a Microsoft Edge Full Chain with Logic DEVCORE Research Team researcher Orange Tsai chained four logic bugs into a Microsoft Edge sandbox escape at Pwn2Own Berlin 2026, earning $175,000 and 17.5 Master of Pwn points, the TrendAI Zero Day Initiative confirmed on May 14, 2026. The full chain comprised five bugs and two Windows features and contained no memory corruption, and it won the Epic Achievement award at this year's Pwnie Awards. Tsai said the work was built entirely by hand with no AI, and published the bug-hunting and chaining process in slides from HITCON and Hexacon. I think I should still dump some of my work once in a while, even though it feels like fewer and fewer people actually sit down and read articles these days or maybe now they all leave it to their agents XD . Anyway, this is what I built for Pwn2Own Berlin 2026 — a completely handmade, logic-only Microsoft Edge Full Chain, with no memory corruption, and no AI at all 🔥 Confirmed Orange Tsai @orange 8361 of DEVCORE Research Team @d3vc0r3 chained 4 logic bugs to achieve a sandbox escape on Microsoft Edge, earning $175,000 and 17.5 Master of Pwn points. Full win Pwn2Own P2OBerlin https://t.co/vJaIpGG4cD — TrendAI Zero Day Initiative @thezdi May 14, 2026 https://x.com/thezdi/status/2054868495888777266?ref src=twsrc%5Etfw The whole chain is made up of five bugs and two Windows features. I have also shared the whole bug-hunting and chaining process at this year’s HITCON and Hexacon, plus some fun stories along the way. You can check the slides here https://github.com/orangetw/My-Presentation-Slides/blob/main/data/2026-The-Last-Proof-of-Human-Craft-en.pdf Also, maybe everyone thinks LLMs can already pwn everything in the AI era, so something made entirely by hand somehow feels a bit more precious. That might also be why this chain was nominated for this year’s Pwnie Awards and won the Epic Achievement award. 🎉 So where do we start with a logic-only full chain? Well, since it’s a “browser,” let’s start by looking at how “browsing” or what Chromium calls “navigation” works. I won’t go into too much detail in this post, because I think in this era, you can just ask your agents to fill in the details : But I’d still recommend reading “ Life of a Navigation https://docs.google.com/presentation/d/1YVqDmbXI0cllpfXD7TuewiexDNZYfwk6fRdmoXJbBlM/edit “ from Chrome University if you want to get the full picture. While I was walking through the Chromium Navigation flow, something called “Navigation Throttles” caught my attention. They live in the Browser Process, and intercept all navigation requests initiated by either the Browser or the Renderer. Maybe it’s easy to say this in hindsight, but out of the 3,000+ CVEs Google Chrome has fixed over the past five years, there are only fewer than 20 related to them. So I guess this is also a corner not too many people pay attention to. Anyway, I started digging into the Navigation Throttles, and checked how Google Chrome and Microsoft Edge implemented them. And while going through the code, I found that Edge has a throttle called EdgeCAGuidanceNavigationThrottle for its enterprise Entra ID feature the CA here stands for Conditional Access https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview . The purpose of this throttle is to guide users to the right User Profile when Edge detects them signing in with a work account on a Microsoft website. And to fire this throttle, you have to start a navigation from a specific Microsoft-owned origin, such as login.live.com : | 1 | location.href = https://guidance-ca-error/?email={email}&switch profile={url} | And once the throttle is fired, Edge would compare the email of the current User Profile with ?email={email} . If they don’t match, Edge pops up a dialog guiding you to switch accounts. But if they do match, Edge would then grab the URL you were originally heading to from switch profile={url} , and launch a new navigation with Chromium’s internal Navigation API: | 12345 | NavigateParams params browser, switch profile url, ui::PAGE TRANSITION LINK ;params.disposition = WindowOpenDisposition::CURRENT TAB;params.window action = NavigateParams::SHOW WINDOW;Navigate ¶ms ; | So basically, the throttle cancels the original navigation when the target URL matches a specific internal format, and then launches a new one with the URL from the attacker-controlled Query String, if the conditions are met. Since this new navigation is initiated by the Browser Process, in theory we can do a few things we couldn’t do before, such as jumping to normally prohibited pages like edge://settings or devtools://... , or even pseudo-schemes like javascript:alert origin . Though the JavaScript pseudo-scheme sounds like a powerful primitive, you still need to obtain the Execution Context on login.live.com to fire the throttle in the first place. So being able to jump to javascript: doesn’t really give you any extra capability. At most, it’s just like a Self-XSS, or a way to turn an Open Redirect there into XSS. Either way, you’re still only executing JavaScript in the same login.live.com context. Honestly, I didn’t take this bug too seriously at first glance. To me, it was just a stronger Open Redirect that could jump to a few more Browser-Level Schemes, and still needed to be chained with other bugs. So the bug just sat in my notes for weeks… 🤦♂️ The turning point was when I was going over the Chromium architecture one day and started thinking: The throttle is handled in the Browser Process, so the new navigation is initiated from the browser as well. However, the page we fired the throttle from, login.live.com , lives in the Renderer, and while investigating the logic, I couldn’t find any code tying the Browser and Renderer together. So… CURRENT TAB refers to which tab, exactly? To find this out, I ran a quick test and added a delay to the original trigger: | 123 | setTimeout function { location.href = https://guidance-ca-error/?email={email}&switch profile={url} }, 8000 | And during the delay, we manually switched the Edge to a new tab to see what would happen, and… Boom We got a UXSS on google.com , and that’s also the first bug of the whole chain. To fully understand the UXSS root cause, we dug into Chromium’s source code. We found that if Navigate is called without specifying the source contents the Renderer source to bind to , Chromium would use the current active tab as the Renderer source automatically. ↓ Path: chromium/chrome/browser/ui/browser navigator.cc https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.97:chrome/browser/ui/browser navigator.cc | 12345678910111213141516171819202122232425262728293031323334353637383940414243444546 | base::WeakPtr