Chaining logic bugs in Microsoft Edge sandbox escape
Microsoft Edge sandbox escape chains do not always need memory corruption. A set of logic flaws can be enough if navigation checks, trust boundaries, and privileged APIs all disagree about what is safe.
Orange Tsai showed that at Pwn2Own Berlin. The chain used four logic bugs, won the browser category, and earned $175,000. The awkward part for defenders is that each bug looked small on its own. Put together, they crossed from browser isolation into privileged code.
Orange Tsai’s Pwn2Own chain and the part that actually mattered
The useful part of the chain was not just that it reached a privileged page. It was that untrusted input could influence navigation while the browser believed it was still dealing with a trusted source.
That pattern matters because browser sandboxing often assumes the renderer can be noisy but not decisive. Once the browser process accepts the wrong navigation state, the renderer gets more than it should. The chain then moves from policy violation into something much more practical: access to a privileged page and a path to file write.
The later exploit work also showed why post-event reconstruction was painful. msedge.dll was over 300 MB, the IDB could grow to 5 GB, and more than 32,000 functions changed between versions. That is a grimly familiar problem with modern browser patch analysis. The patch may be precise, but the binary noise is not.
How Edge’s navigation checks let untrusted input reach privileged code
The navigation bugs sat in EdgeCAGuidanceNavigationThrottle::WillStartRequest. That is where the chain stopped being theoretical and started becoming boring in the worst possible way: a few checks that looked strict, but only on the surface.
One issue involved switch_profile handling. In vulnerable code, that parameter directly set the navigation target. Once a control like that exists in the wrong place, the rest of the logic starts carrying the attacker’s intent for free.
The visible URL was not the real trust decision
A separate flaw used GetVisibleURL to validate the source of a navigation. That sounds tidy until you remember that a visible tab label is not the same thing as the initiator origin.
If the browser checks what the page appears to be rather than what initiated the request, an attacker can separate display from trust. The user sees one URL. The navigation decision comes from another context. That gap is exactly where browser isolation gets weak.
A scheme and host check that only looked strict on paper
The other check was just as awkward. It focused on host matching and accepted https://guidance-ca-error/, which should never have been enough to reach privileged CA Guidance logic.
That sort of validation failure matters because it leaves a narrow path open in a place that looks closed. A renderer could not directly navigate to edge://guidance-ca-error, but it could still reach the privileged processing path through the looser gate. The scheme check and the host check were doing different jobs, and neither was strong enough on its own.
From privileged page access to file write
Once JavaScript could run on a privileged page, the rest of the chain turned into a more familiar abuse path: call a privileged API, then abuse file handling.
The feedback component exposed edgeFeedbackPrivate, which allowed a file write through path traversal. That kind of bug is ugly because it gives a clean primitive in a place that should never offer one. A browser sandbox escape does not need much more than that if the attacker can still shape the path.
edgeFeedbackPrivate and the path-traversal write
The key detail is that the API lived behind a privileged page requirement. That should have been the brake. It was not, because the navigation bugs had already broken the trust model that protected the page in the first place.
From there, arbitrary file write becomes a serious endpoint rather than a curiosity. On Windows, that can be pushed towards remote code execution with extra exploitation work. The chain did not need to finish there to be dangerous. A write primitive in a browser context is already far beyond normal sandbox behaviour.
Why the patched binary made reconstruction so ugly
Patch analysis was messy because the affected module and its symbol database were huge, and the function churn was high. Traditional diffing stopped being a neat before-and-after exercise and became a search problem across a very large codebase.
That matters for defenders too. When logic bugs are spread across unrelated modules, isolated function analysis misses the chain. A check in navigation, a trust decision in UI state, and a privileged file write API may look separate in the source tree. In practice, they behave like one attack surface.
What this escape says about hardening Edge and similar browser paths
This chain is a reminder that browser isolation fails at the joins. A sandbox can hold up against memory safety bugs and still fall over if the browser process trusts the wrong metadata.
Separate origin checks from displayed state
Displayed state should never be the trust decision. GetVisibleURL exists for user-facing behaviour, not for deciding whether untrusted input may enter privileged code.
A safer pattern is simple enough to state and awkward enough to implement properly: validate initiator origin, scheme, and target separately, then compare the values that matter. If the display state and the security state can drift apart, an attacker will try to make them do exactly that.
Test the dump path, not the policy wording
The wording in the policy can read fine while the implementation still routes attacker-controlled data into a privileged branch. So test the actual path a request takes through the browser, not just the condition that claims to guard it.
That means checking what happens when a renderer touches a privileged navigation path, what URL the code really trusts, and whether a helper API hands out a file write where it should not. Logic bugs love components that look unrelated. Browser hardening has to treat them as connected by default.

