CVE-2026-53502: thumbor Path Traversal via Post-Validation URL Decode in file_loader
Thumbor's file loader checks the resolved path for safety before decoding percent-encoded characters, letting an attacker use encoded dot-sequences to slip past the check and read any file on the…

The problem
In thumbor's file_loader, the root-path confinement check runs on the raw (still-encoded) path. The call to unquote() happens only afterwards, when thumbor retries with the decoded path because the encoded one does not exist on disk.
An attacker who controls a filter argument (watermark, frame) can supply double-encoded traversal sequences that survive the abspath() + startswith() guard as harmless literal directory names, then get decoded to real .. components and resolved by the OS. This gives unauthenticated arbitrary file read on any thumbor instance using file_loader with the default ALLOW_UNSAFE_URL = True.
Proof of concept
A working proof-of-concept for CVE-2026-53502 in thumbor, with the exact payload below.
# %252e is double-encoded: Tornado decodes %25 -> %, giving %2e to file_loader.
# file_loader unquote() then decodes %2e -> . producing real '..' traversal.
curl 'http://thumbor-host:8888/unsafe/filters:watermark(%252e%252e/%252e%252e/%252e%252e/%252e%252e/etc/passwd,0,0,100)/some-valid-image.jpg'The root cause is a TOCTOU-style ordering bug (CWE-22). The security check calls abspath() on the joined path and verifies it starts with FILE_LOADER_ROOT_PATH, but %2e%2e is not a dot-dot at that point, so abspath() leaves it alone and the check passes.
Because no literal %2e%2e directory exists, exists() returns False and the code falls through to unquote(file_path), which converts %2e to .. The OS then resolves the real ../../../etc/passwd traversal and opens the file.
The watermark and frame filters are the practical entry point because they pass their URL argument directly to file_loader.load() without re-encoding it, unlike the main image URL path which applies quote() in imaging.py. The patch (commit 3b986d1) moves unquote() to before the abspath() + startswith() check, so the fully decoded path is always what gets validated.
The fix
Upgrade to thumbor 7.8.0 (pip install --upgrade thumbor). The fix moves unquote() to before the confinement check in thumbor/loaders/file_loader.py so the resolved, decoded path is always what is validated. If upgrading is not immediately possible, disable the watermark and frame filters by removing them from FILTERS in your thumbor config, or switch from file_loader to http_loader.
Related research
- high · 8.2CVE-2026-53501CVE-2026-53501: thumbor HMAC Signature Validation Bypass via Repeated URL Segments
- high · 7.5CVE-2026-53503CVE-2026-53503: thumbor convolution filter remote divide-by-zero DoS
- high · 7.5CVE-2026-53504CVE-2026-53504: thumbor ReDoS in convolution Filter
- high · 7.5CVE-2026-53505CVE-2026-53505: thumbor proportion filter Remote Denial of Service