CWE-647 describes an application that makes authorization decisions on a URL it assumes is already in canonical form. If the check only recognizes one spelling of a path, an attacker can request the same resource with a different spelling and walk past the rule. It hits anything that guards URLs by string or pattern: web servers, reverse proxies, API gateways and application middleware.
How it happens
Suppose a policy says only admins may reach http://www.example.com/mypage. The rule is a string comparison. The same page is also reachable at addresses MITRE lists as equivalent:
http://WWW.EXAMPLE.COM/mypage(different case in the host)http://www.example.com/%6Dypage(alternate encoding of one character)http://192.168.1.1/mypage(the IP address instead of the name)http://www.example.com/mypage/(a trailing slash)http://www.example.com:80/mypage(an explicit default port)
The server resolves all of these to the same resource, but the authorization layer only matched the first. Two components disagree about what the URL means, and the attacker lives in the gap.
Here is a vulnerable pattern in Python:
# Vulnerable: compares the raw path string
PROTECTED = {"/admin", "/admin/users"}
def is_protected(path: str) -> bool:
return path in PROTECTED
A request for /%61dmin or /admin/ is not in the set, so the check returns False while the file server still serves the admin page.
What an attacker gains
MITRE lists two consequences. The first is an access control bypass: the attacker reaches a URL that should have been protected. The second is reading files or directories. When a non-canonical URL is used, the server may return the contents of a file instead of pre-processing it, for example returning source rather than running it as a program.
How to find it
Static analysis (SAST) can find some instances by tracing where request paths reach an authorization check without being normalized first. The simplest manual test is to take every protected URL and request it with the variants above: mixed case, percent-encoded letters, a trailing slash, doubled slashes, dot segments, and the raw IP. Any variant that returns content instead of a denial is a finding.
How to fix it
MITRE's mitigations are two steps at the design stage.
- Make access control policy based on path information in canonical form. Validate that the path is in the expected form with very restrictive regular expressions.
- Reject all alternate path encodings that are not in the expected canonical form. A default deny rule gets you most of the way there.
In code, decode and normalize once, early, then compare:
from urllib.parse import unquote
import posixpath
def canonical(path: str) -> str:
p = posixpath.normpath(unquote(path))
return p.lower().rstrip("/") or "/"
def is_protected(path: str) -> bool:
c = canonical(path)
return c == "/admin" or c.startswith("/admin/")
Better still, deny by default and allow only named public paths, so a spelling you did not think of fails closed. Make sure the authorization layer and the component that serves the file use the same normalization.
Quick check
Why is a blocklist of protected URLs a weak design here?
A blocklist only denies the spellings you thought of. Any equivalent spelling you missed is allowed. A default deny allowlist on the canonical path fails closed instead.
Is blocking only the percent sign enough?
No. MITRE's list also includes host case, IP address form, trailing slash and explicit port. Normalize all of them, then reject anything that is not in the expected form.