How One Parsing Bug Outlived Three Firefox Patches
CVE-2026-8971 · Same Origin Policy Bypass · Firefox Networking: JAR
I reported this exact bug class twice already. Mozilla fixed the string handling. Mozilla fixed the content sniffing. And the null byte still got through, this time wearing a different protocol’s clothes.
That was the moment I knew I wasn’t filing a duplicate. I was filing the third chapter of a story about how hard it actually is to kill a parsing bug once it has a foothold in a codebase this large.
CVE-2026-8971 is a same origin policy bypass in Firefox’s Networking: JAR component. A null byte, sent as %00, embedded in a jar: or resource:/// URL path survives Firefox’s URL parsing and reaches code further downstream that never expected to see one. Depending on where it lands, this produces two separate effects: a spoofed download filename where the attacker fully controls what the user sees in the save dialog, and a MIME type confusion where Firefox serves a resource under a content type that has nothing to do with the file’s actual extension or content.
The same origin policy classification is worth unpacking rather than taking on faith. The jar: scheme assigns origin and trust based on the inner resource’s identity, meaning the JAR entry name and its resolved MIME type are part of how Firefox decides what that resource is and how to treat it. When a null byte truncates or redirects how that entry name resolves, the browser ends up associating one resource’s origin context, an image.jpg entry inside a trusted local archive, or a resource bundled inside a legitimately installed extension, with content the browser actually serves under a different identity.
That mismatch between declared origin and served identity is the same origin policy violation. It is not that script from one origin reads data from another in the classic SOP bypass sense. It is that the browser’s own resolution layer misattributes which origin a piece of content belongs to, and everything downstream, including download handling and content type negotiation, inherits that wrong attribution.
The moz-extension:// to resource:/// to jar: chain makes this concrete: a wildcard web_accessible_resources entry is explicitly the extension author saying “trust requests for these paths from web content,” and the null byte lets an attacker redirect that trust to a path the author never intended to expose, while the response still carries the extension’s origin.
The fix landed in Firefox 151. Mozilla rated the impact low and classified it as a same origin policy bypass in the Networking: JAR component. It is not a flashy memory corruption bug and it will not get a high CVSS headline. What makes it worth writing about is the lineage.
This is the third CVE in the same vulnerability class, in the same component, against the same root cause, in about a year and a half. CVE-2025-1936 came first. CVE-2026-2790 bypassed that fix. CVE-2026-8971 bypassed the fix for the bypass. A bypass of a bypass, and the null byte still walked through the front door.
I did not go looking for this. I was revisiting CVE-2026-2790, the second vulnerability in this chain, after Mozilla shipped the fix that disabled content sniffing for JAR channels. I wanted to confirm the fix actually closed the door I had walked through with the double encoded null byte trick.
My hypothesis going in was narrow: check whether disabling sniffing also stopped the filename level tricks, or whether it only stopped the script execution path. Sniffing and filename resolution are different code paths in a browser, and patches that close one rarely close the other by accident. I expected to find the sniffing disabled cleanly and the rest of the surface untouched.
I was right, but not in the way I expected. The fix held up against the exact double encoding technique from the previous bug. But the moment I went back to basics and tried a single, non double encoded %00 directly in a jar: path, against a part of the URL the two previous patches had not touched, it still worked.
My approach this time was deliberately boring. Instead of inventing a new technique, I went back through every variant of null byte placement that the first two bugs had not explicitly tested, and ran each one against a fresh, fully patched Nightly build.
I built test archives with a small Python script using the standard zipfile module, nothing exotic, just a couple of dummy entries named image.jpg and style.css inside a test.zip. Then I drove Firefox’s address bar directly, trying jar:file:/// paths with a raw %00 planted at different points in the entry path rather than the file’s actual archive entry name. That distinction mattered.
The first bug put the null in the JAR entry name itself. The second put it in a double encoded form of that same entry name. I wanted to know what happened if the null sat in the path segment after the !/ separator, untouched by either previous fix.
The early signal came fast. Navigating to jar:file:///path/to/archive.zip!/%00://evil.exe produced a download prompt, and the filename Firefox offered to save was evil.exe, fully attacker chosen, regardless of what was actually inside the archive. That told me the download filename logic was still trusting whatever came after the null, completely independent of the content sniffing fix.
Then I went after MIME resolution directly. A URL like jar:file:///C:/Users/<user>/Desktop/test.zip!/%00style.css.html got served back as text/html. A variant ending in .xml triggered Firefox’s own XML parsing error page, which was actually the most useful negative result in the whole investigation, because it proved Firefox was deriving the content type from the extension sitting after the null byte rather than from the real file.
The sniffing fix from CVE-2026-2790 was working exactly as designed. It just was not the only place a null byte could cause this.
I also checked resource:///, since I remembered from the first bug’s thread that Rob Wu at Mozilla had pointed out it resolves internally to jar: URLs. Same result: resource:///%00.html and resource:///%00test/ both showed the same null byte leakage. That mattered more than it might look, because resource:/// is the protocol moz-extension:// resolves to internally, and moz-extension:// is reachable from ordinary web content when an extension exposes wildcard entries in web_accessible_resources.
A page running window.location = “moz-extension://<uuid>/logo.png%00://update.exe” could turn a legitimate, already installed extension into the apparent source of a file named update.exe.
The first wall was convincing myself this was not just CVE-2026-2790 again with extra steps. I spent a fair amount of time re-reading my own old bug report and the patch notes for the sniffing fix before I trusted that I was looking at a genuinely different code path. It is easy to see a null byte do something unexpected and assume you have rediscovered your own bug.
I had to isolate the filename spoofing behavior from the MIME confusion behavior and prove each one independently, which meant building separate minimal test cases for each rather than one combined proof that could be explained away as overlap with the old bug.
The second wall was the moz-extension angle. I could reproduce the resource:/// behavior easily enough, but proving it was reachable from actual web content, not just something I could trigger by typing into the address bar, took longer.
Address bar navigation and remote triggering are treated very differently in Firefox’s security model, and a bug that only works when a user manually pastes a crafted URL is a much weaker finding than one a malicious page can trigger on its own. I leaned on the prior art from CVE-2025-1936 here, since Rob Wu had already mapped out the wildcard web_accessible_resources angle in detail on that earlier disclosure, and I did not need to rediscover it from scratch.
There was also a quieter frustration running underneath all of this. I had already gone through the triage process twice. I knew Mozilla would ask the same root cause question they asked on the first bug: is this really exploitable, or is it just an internal protocol quirk nobody can reach.
I spent time tightening the writeup before I even filed it, trying to preempt that back and forth, because I had lived through how long it can take to get past it.
The moment of clarity was realizing both previous fixes had treated the null byte as a local problem to be solved at a specific function, rather than as an input that should never have been considered valid in the first place. CVE-2025-1936’s fix swapped char* and strlen for nsACString inside the JAR entry handling code, which stopped the null from silently truncating the string in that one place. CVE-2026-2790’s fix turned off content sniffing for JAR channels entirely, which stopped script execution through the double encoding trick.
Each fix closed the exact door the previous bug had walked through, and each time the null byte simply used a different door. That is the part that makes this a bypass of a bypass rather than two unrelated bugs. The underlying defect was the same the whole time.
The root cause, once I had isolated it cleanly, was straightforward: nothing at the point where Firefox parses a jar: or resource:/// URL rejects an embedded %00. Each fix so far has been a plug at a specific point where that null byte caused visible damage. The filename resolution logic and the MIME type derivation logic are two more such points, previously unpatched, and there is no guarantee they are the last two.
Conceptually, the exploitation conditions break into two scenarios. For the local file vector, an attacker needs a victim to open a crafted jar:file:/// URL pointing at an archive the attacker controls the path of, which gets a spoofed download filename with an extension of the attacker’s choosing regardless of the archive’s real contents. For the web reachable vector, an attacker needs a target browser extension that exposes a wildcard entry in web_accessible_resources, since that is what makes the moz-extension:// to resource:/// to jar: chain reachable from a normal web page rather than only from manual address bar entry.
In that second scenario, a script on any page can redirect the browser to a moz-extension:// URL belonging to an installed extension, append a null byte and a fake filename, and have Firefox present a download dialog that appears to originate from that legitimate extension.
The impact, in plain terms, is convincing a user to download and run something they believe is harmless, with the legitimate sounding origin doing the social engineering for the attacker. But the SOP bypass classification matters beyond labeling. Same origin policy is the mechanism that keeps a page or resource from one trust boundary from being treated as though it belongs to another.
Here, the trust boundary is the extension’s declared web_accessible_resources surface, the explicit, narrow set of paths an extension author chose to expose to the web. The null byte lets an attacker step outside that declared surface while the browser still attributes the response to the extension’s origin, because origin attribution for jar: backed resources runs through the same entry resolution logic the null byte corrupts. It is not a sandbox escape and it is not remote code execution on its own.
It is an origin attribution failure that gets weaponized into a trust manipulation primitive, and those are exactly the kind of bugs that end up as the first step in a longer chain rather than the final payload.
| Reported | April 16, 2026, via Mozilla Bugzilla |
| Patch landed | April 21, 2026 |
| Bounty awarded | April 22, 2026 |
| Fixed in | Firefox 151 |
| CVE assigned | CVE-2026-8971 |
| Public disclosure | May 19, 2026, via MFSA2026-46 |
The fix landed five days after I reported it, which is a fast turnaround for a bug that required cross referencing two prior CVEs to evaluate properly. The triage on this one was noticeably smoother than my first report in this series. By the third bug in a known chain, the team already had the context to move quickly, and that context made a real difference.
The actual code change, checking JAR entry names for embedded nulls at the point of resolution rather than further downstream, was small and low risk by Mozilla’s own assessment, which lines up with how narrow the remaining gap actually was once you saw it clearly.
If you are reviewing how your own software handles untrusted paths or filenames, the pattern here is worth internalizing: a null byte fix applied at the function where the bug was discovered does not mean the input has been rejected, it means that one consumer of the input has been hardened. If the same unsanitized value reaches a second consumer, the bug reappears wearing a different effect.
The practical test I would suggest is tracing every place a percent decoded string is consumed after it leaves your URL parser, not just the place where you found the original issue. In this case that meant the JAR entry lookup, the content sniffing logic, the filename resolution code, and the MIME type derivation code were all separate consumers of the exact same tainted string, and each one needed its own independent compromise before the underlying input validation gap actually closed.
The other transferable point is about internal protocol aliasing. moz-extension:// looking like a self contained, safe internal scheme hid the fact that it was just a thin wrapper around resource:///, which was itself a thin wrapper around jar:. Any time an internal scheme is documented as resolving to another scheme under the hood, assume vulnerabilities in the underlying scheme apply to the wrapper too, and test for them explicitly rather than assuming the wrapper layer sanitizes anything.
Two bypasses on the same byte taught me that fixing the symptom you can see is not the same as fixing the input you should have rejected. I will be going back through the rest of Firefox’s internal protocol handlers with that lens, since JAR is unlikely to be the only place this pattern exists.
If your team is shipping a product with its own custom URL schemes, internal protocol handlers, or browser extension surfaces, this exact failure mode is worth testing for before someone else finds it. Payatu’s Product Security Assessment is built for exactly this kind of deep, parser level review, the sort that catches the gap between what a patch fixes and what the original bug actually was.