# Thumbor has path traversal via post-validation URL decoding bypass in file_loader

Thumbor's file_loader unquotes the path after its abspath/startswith check, so double-encoded %2e%2e in the watermark filter escapes the root and reads arbitrary files.

- Payout: Research
- Severity: high
- Program: Thumbor
- Found by: q1uf3ng, 0xHunSec
- Disclosed: 2026-07-31
- CVE: CVE-2026-53502
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-cj54-hpcc-gj6h
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-cj54-hpcc-gj6h
- Tags: area:web domain:web impact:data-leak kind:research platform:ghsa sev:high tech:python technique:chained technique:source-review vuln:file-upload vuln:path-traversal

**Target:** thumbor file_loader via watermark/frame filter URL parameters
**Root cause:** file_loader calls unquote() on the path only after the abspath()+startswith() containment check, so percent-encoded traversal passes validation as literal directory names and is decoded to '..' afterwards. Watermark/frame filters forward URL params without re-encoding, unlike the main image flow.
**Steps:**
1. Configure LOADER=thumbor.loaders.file_loader with ALLOW_UNSAFE_URL=True (default) and watermark filter enabled (default).
2. Send `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'`.
3. Tornado decodes %252e to %2e; watermark filter passes `%2e%2e/.../etc/passwd` to file_loader.
4. join(ROOT, path) yields ROOT/%2e%2e/...; abspath treats %2e%2e as a literal dir so startswith(ROOT) passes.
5. exists() is False for the literal path, triggering unquote() which turns %2e%2e into '..'.
6. OS resolves '..' and opens the file outside ROOT.
**How found:** Noticed the ordering anomaly: unquote() runs after the abspath/startswith containment check, and that watermark/frame filters pass URL params to the loader without the quote() re-encoding applied in imaging.py.
**Exploitation:** Unauthenticated GET to the thumbor endpoint with a double-encoded traversal in the watermark filter path; the target file is read and, if a valid image, returned overlaid as a watermark in the response. Enables reading configs, secrets and private keys.
**Insight:** When a sanitizer/containment check runs before a decoding step, look for double-encoded input (%252e) that survives validation as a literal and decodes later; also compare every caller of a loader for missing re-encoding versus the main flow.

## Proof of concept (copied exactly from the original report)

**curl request exploiting watermark path 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'
```

