The issue is that the function normalizes the URI path before percent decoding it:
let resolve_local_file ~docroot ~uri =
let path = Uri.(pct_decode (path (resolve "http" (of_string "/") uri))) in
...
Because %2f is decoded after Uri.resolve, encoded separators survive
dot-segment normalization. For example, a request path like:
/static/..%2f..%2f..%2fetc/passwd
is normalized as a single encoded segment, then decoded into:
/static/../../../etc/passwd
afterwards.
{
"cwe": [
"CWE-22"
],
"human_link": "https://github.com/ocaml/security-advisories/tree/main/advisories/2026/OSEC-2026-16.md",
"osv": "https://github.com/ocaml/security-advisories/tree/generated-osv/2026/OSEC-2026-16.json"
}