How parser-differential auth bypasses keep getting shipped
On 2026-09-30 CISA added CVE-2026-76504 to the Known Exploited Vulnerabilities catalogue. The entry is terse: Cisco Catalyst SD-WAN Manager contains a hex encoding vulnerability that could allow an unauthenticated, remote attacker to access an affected system with privileges of the admin user due to improper handling of URI encoding in an HTTP request. Cisco's own advisory sharpens the mechanism: a crafted HTTP request to the API bypasses an authentication rule intended to restrict access to a specific endpoint.
Twenty-four hours later a near-identical shape landed in the catalogue. CVE-2026-104286 in Fortinet FortiMail is tagged as a path traversal and an improper neutralization of a NULL byte that lets an unauthenticated attacker write arbitrary files through crafted HTTP or HTTPS requests. Different vendor, different product tier, same underlying failure.
Both bugs are instances of a vulnerability class the industry has shipped for twenty years: parser differentials in pre-authentication HTTP handling. One component (the auth filter, the WAF rule, the routing table) reads the request one way. A second component (the business handler, the file I/O layer, the SQL driver) reads it another way. The attacker exploits the gap between the two readings.
What the class actually is
A web application receives bytes. Those bytes become a URI, a header map, a body. Many software frames touch the request before it reaches the privileged handler: a reverse proxy, a web server, an authentication filter, a routing table, a controller, a view, a model. Each frame has its own parser. Those parsers agree most of the time. The bugs live where they disagree.
The canonical dangerous shape is:
- Component A (auth) inspects some canonicalised form of the request and decides whether to allow it to proceed.
- Component A forwards the original bytes, not its canonical form, to component B (handler).
- Component B parses the bytes differently and takes an action that A would have blocked had A read it the same way.
In the Catalyst SD-WAN Manager finding the ambiguous surface is URI percent (hex) encoding. In the FortiMail finding it is NULL byte handling in a path. The underlying violation is identical: trust decisions were made against one interpretation of a string that a downstream frame interprets differently.
Why percent-encoding keeps shipping bugs
Percent-encoding looks simple. %2e is ., %2f is /, %00 is NULL. RFC 3986 says a generic URI parser should treat the decoded and encoded forms as equivalent for most unreserved characters, and should distinguish them for reserved ones. That rule is routinely violated at system boundaries because engineers assume someone else already decoded the string.
A minimal example of the pattern, deployed against api.example.com:
# auth filter: literal string match on the raw path
RESTRICTED = {"/admin/debug", "/admin/shell"}
def is_allowed(path: str, session) -> bool:
if path in RESTRICTED and not session.is_admin:
return False
return True
# handler: receives the same raw path, decodes it, dispatches
def dispatch(path: str, session):
if not is_allowed(path, session):
abort(403)
decoded = urllib.parse.unquote(path) # %2f -> /, %64 -> d
route(decoded, session)
A request to /admin/debug is denied for a non-admin. A request to /admin/%64ebug passes the literal-string check in is_allowed, then decodes to /admin/debug inside route. The handler executes the privileged path, the filter has already approved it, and the attacker reaches admin access without authenticating.
Hex encoding is one axis. Case folding, backslash normalisation, Unicode compatibility forms (NFKC vs NFC), overlong UTF-8, multiple slashes, trailing dots, and the empty segment all represent the same class of disagreement. Any filter that compares strings before all downstream frames have agreed on a canonical form is a candidate for the next catalogue entry.
Why NULL bytes keep shipping bugs
The FortiMail entry calls out improper neutralisation of NULL in conjunction with path traversal. NULL byte issues are a special case of the same differential: a high-level language treats a string as length-prefixed and a lower layer treats it as NULL-terminated. A request containing ..%2f..%2fetc%2fpasswd%00.png can pass an extension allow-list in the application tier (the string ends in .png) and then be opened by a syscall that sees only /etc/passwd. Combine that with path traversal and an unauthenticated handler, and arbitrary file write becomes arbitrary code execution once the attacker places bytes in a path the service later executes, loads, or interprets.
How to tell whether you are exposed
Treat parser-differential bugs as a class, not a list of CVEs. For each HTTP-facing service in scope:
- Enumerate every frame that touches the request before it reaches the handler: load balancer, reverse proxy, WAF, auth filter, router, controller. Any pair of adjacent frames is a candidate boundary.
- For every allow or deny rule written against path or header strings, confirm whether the comparison runs on raw bytes, a percent-decoded form, or a Unicode-normalised form. Document that assumption beside the rule.
- Fuzz the boundary. Send the same semantic request in multiple encodings against a staging copy:
/admin/x,/admin/%78,/admin/%2fx,/admin/x%00.png,/admin//x,/admin/./x,/admin/%2e/x. Confirm that every encoding produces the same authorisation outcome. - For file-handling endpoints, log the exact bytes passed to the lowest syscall (
open,read,write) and compare against what the application-tier allow-list evaluated. Any disagreement is a finding, whether or not you can yet articulate the exploit.
Both vendors have shipped fixes. Patch Catalyst SD-WAN Manager and FortiMail to the versions named in the advisories you receive from the vendor. Both entries fall under CISA's BOD 26-04 (Prioritising Security Updates Based on Risk, effective 2026-06-10), which carries the KEV criteria forward from the superseded BOD 22-01. Follow the due date your KEV feed carries for each entry; do not substitute a self-imposed timer for an authoritative one.
The structural lesson
Each of these bugs ships because the auth decision is made against a model of the request the handler does not share. The fix at the class level is not another rule bolted onto the filter; it is a single canonical form established at the edge, enforced before any trust decision, and re-asserted at every downstream frame. Pass the canonical form, not the raw bytes, to the handler, leaving no second interpretation to diverge from the first. If your auth tier and your business tier parse the same bytes differently, you have a class of CVE-2026-76504 and CVE-2026-104286 waiting to be written against you.
Verifiable security.