# Thumbor has HMAC validation bypass via multiple .replace() calls when removing URL signature

Thumbor strips the HMAC signature with global .replace(), so duplicating the signature in the path makes the validated URL differ from the fetched one, bypassing domain restrictions.

- Payout: Research
- Severity: high
- Program: Thumbor
- Found by: caioluders
- Disclosed: 2026-07-31
- CVE: CVE-2026-53501
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-mw3h-qjxj-6xg9
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-mw3h-qjxj-6xg9
- Tags: area:web domain:web impact:internal-access kind:research platform:ghsa sev:high tech:python technique:parser-differential technique:source-review vuln:auth-bypass vuln:ssrf

**Target:** Thumbor image URL signing / HMAC validation
**Root cause:** Signature removal uses Python str.replace() without a count, deleting every occurrence of the signature substring in the URL; injected duplicate copies alter the string that gets HMAC-validated versus the path actually fetched.
**Steps:**
1. Note valid signed URL `/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.com/v1/.../image.jpg`.
2. Inject the same hash inside the path: `/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.co/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/m/v1/.../image.jpg`.
3. Thumbor's `.replace(f"/{hash}/", "")` removes all occurrences, so `url_to_validate` differs from the effective request path.
4. Use the second `.replace()` for the quoted hash (`%3D`) to further manipulate the path, e.g. `/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.com/ddoyYVYUbDf6Po/ddoyYVYUbDf6Po_dzOrBhCDrXLc%3D/ddoyYVYUbDf6Po/v1/...`.
5. Request the crafted URL; HMAC validates against the stripped string while Thumbor fetches the attacker-chosen host/path.
**How found:** Code review of the validation path: noticed `.replace()` is called without a count on a string that itself contains the signature, implying all occurrences are stripped and the validated string can be decoupled from the requested path.
**Exploitation:** Attacker crafts a URL containing extra copies of a legitimately signed hash (and its URL-encoded form) so that after global replacement the validated string matches the original signed resource, while the actual upstream host/path resolves to an arbitrary domain; Thumbor then fetches the unintended image, acting as an open proxy.
**Insight:** When a signature/token is embedded in the path and stripped by string replacement before verification, test whether the token can be duplicated so global replacement rewrites the validated string; also check the URL-encoded variant of the token for a second manipulation primitive.

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

**Signed URL with injected duplicate hash**

```
/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.co/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/m/v1/.../image.jpg
```

**Encoded-hash path manipulation payload**

```
/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.com/ddoyYVYUbDf6Po/ddoyYVYUbDf6Po_dzOrBhCDrXLc%3D/ddoyYVYUbDf6Po/v1/...
```

