FA-211 / Protocols / Open access
A second percent-decoding pass changes path data into syntax · case 01
A literal plus becomes a space, or a double-encoded slash unexpectedly becomes a path separator.
ROOT CAUSE
Form-urlencoded rules or repeated decoding are applied to a path component that requires one decode pass.
VERIFIED REPAIR
Decode percent-encoded bytes exactly once with strict UTF-8 handling; preserve literal plus characters.
Unsuccessful approach: Decoding until the text stops changing interprets encoded percent signs as instructions for another pass.
Case contract
Decode a URL path component once using UTF-8. Preserve literal plus signs and malformed percent tokens; return None when encoded bytes are invalid UTF-8. Do not normalize paths or separators.
Why this case matters
Different decoding layers can disagree about a resource path. Keeping the decode count and plus-sign convention explicit prevents ambiguous interpretation.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
from urllib.parse import unquote, unquote_plus
N = 1
observations = []
def solve(text):
try:
return unquote_plus(text, errors='strict')
except UnicodeDecodeError:
return None
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
label = 'a' * N
check('literal plus retained in path', solve(label + '+b'), label + '+b')
check('encoded slash decoded once', solve(label + '%2Fb'), label + '/b')
check('double-encoded slash remains encoded', solve(label + '%252Fb'), label + '%2Fb')
check('encoded plus becomes literal plus', solve('%2B' * N), '+' * N)
check('UTF-8 encoded character', solve('%C3%A9' * N), 'é' * N)
check('malformed percent token preserved', solve(label + '%QZ%'), label + '%QZ%')
check('invalid UTF-8 rejected', solve('%FF'), None)
check('empty path component', solve(''), '')
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| literal plus retained in path | a b | a+b | Failed |
| encoded slash decoded once | a/b | a/b | Passed |
| double-encoded slash remains encoded | a%2Fb | a%2Fb | Passed |
| encoded plus becomes literal plus | + | + | Passed |
| UTF-8 encoded character | é | é | Passed |
| malformed percent token preserved | a%QZ% | a%QZ% | Passed |
| invalid UTF-8 rejected | None | None | Passed |
| empty path component | | | Passed |
SHA-256 / b41b6d0e7846f34fc295afae1d547210b3d90a3352c1335006b90687471090bb
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
from urllib.parse import unquote, unquote_plus
N = 1
observations = []
def solve(text):
try:
while True:
decoded = unquote(text, errors='strict')
if decoded == text:
return decoded
text = decoded
except UnicodeDecodeError:
return None
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
label = 'a' * N
check('literal plus retained in path', solve(label + '+b'), label + '+b')
check('encoded slash decoded once', solve(label + '%2Fb'), label + '/b')
check('double-encoded slash remains encoded', solve(label + '%252Fb'), label + '%2Fb')
check('encoded plus becomes literal plus', solve('%2B' * N), '+' * N)
check('UTF-8 encoded character', solve('%C3%A9' * N), 'é' * N)
check('malformed percent token preserved', solve(label + '%QZ%'), label + '%QZ%')
check('invalid UTF-8 rejected', solve('%FF'), None)
check('empty path component', solve(''), '')
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| literal plus retained in path | a+b | a+b | Passed |
| encoded slash decoded once | a/b | a/b | Passed |
| double-encoded slash remains encoded | a/b | a%2Fb | Failed |
| encoded plus becomes literal plus | + | + | Passed |
| UTF-8 encoded character | é | é | Passed |
| malformed percent token preserved | a%QZ% | a%QZ% | Passed |
| invalid UTF-8 rejected | None | None | Passed |
| empty path component | | | Passed |
SHA-256 / e18f6174cecfbd2d7bcaf9d060e3468a509dcaa12bac9b62a9fe346ae468b78c
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
from urllib.parse import unquote, unquote_plus
N = 1
observations = []
def solve(text):
try:
return unquote(text, errors='strict')
except UnicodeDecodeError:
return None
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
label = 'a' * N
check('literal plus retained in path', solve(label + '+b'), label + '+b')
check('encoded slash decoded once', solve(label + '%2Fb'), label + '/b')
check('double-encoded slash remains encoded', solve(label + '%252Fb'), label + '%2Fb')
check('encoded plus becomes literal plus', solve('%2B' * N), '+' * N)
check('UTF-8 encoded character', solve('%C3%A9' * N), 'é' * N)
check('malformed percent token preserved', solve(label + '%QZ%'), label + '%QZ%')
check('invalid UTF-8 rejected', solve('%FF'), None)
check('empty path component', solve(''), '')
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| literal plus retained in path | a+b | a+b | Passed |
| encoded slash decoded once | a/b | a/b | Passed |
| double-encoded slash remains encoded | a%2Fb | a%2Fb | Passed |
| encoded plus becomes literal plus | + | + | Passed |
| UTF-8 encoded character | é | é | Passed |
| malformed percent token preserved | a%QZ% | a%QZ% | Passed |
| invalid UTF-8 rejected | None | None | Passed |
| empty path component | | | Passed |
SHA-256 / 3150261a0255d06a41aedbe0409319a87636e7f76679f4390fc13158d2019935
Verification & scope
This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:36:51.476199+00:00.
Case digest / 67f45cf34b1344a1fd642d03fa1322c01378ee69bbb69b2b1eec3dcfeebca8bc