Thumbor treats ALLOWED_SOURCES string patterns as unescaped regex, allowing hostname bypass via wildcard dot
What fixes it
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
The ALLOWED_SOURCES configuration is meant to restrict which hosts Thumbor's HTTP loader may fetch images from. Plain-string entries in that list (the overwhelming majority of real-world and documented configurations) are passed directly to re.match() without escaping. Because . is a regex wildcard, every dot in a domain name becomes a bypass vector: s.glbimg.com silently matches sXglbimgYcom, sAglbimg.com, and any other hostname that differs only at a dot position. This undermines the primary SSRF defence that ALLOWED_SOURCES is intended to provide.
Affected component
thumbor/loaders/http_loader.py — validate()
Proof of concept
import re from thumbor.config import Config from thumbor.context import Context from thumbor.loaders import http_loader as loader config = Config() config.ALLOWED_SOURCES = ["s.glbimg.com"] # typical user config ctx = Context(None, config, None) # These should be blocked — both return True due to the unescaped dot print(loader.validate(ctx, "http://sXglbimgYcom/secret.jpg")) # True ← bypass print(loader.validate(ctx, "http://sAglbimg.com/secret.jpg")) # True ← bypass # Legitimate origin — correctly allowed print(loader.validate(ctx, "http://s.glbimg.com/logo.jpg")) # True ← correct
Root cause
thumbor/loaders/http_loader.py (before fix):
for pattern in context.config.ALLOWED_SOURCES:
if isinstance(pattern, Pattern):
match = url
else:
pattern = f"^{pattern}$" # <-- dots not escaped, act as regex wildcard
match = res.hostname
if re.match(pattern, match):
return TrueImpact
An attacker who can influence the image source URL passed to Thumbor can fetch images from arbitrary hosts, bypassing the ALLOWED_SOURCES allowlist.
Preconditions:
ALLOWED_SOURCEScontains at least one plain-string entry (the common case; all official documentation examples use plain strings).- The attacker can supply or influence the image URL — true whenever
ALLOW_UNSAFE_URL = True(the default), or when the application forwards user input to a signed URL endpoint.
Fix
Apply re.escape() to plain-string patterns before compiling them, so every character is matched literally:
else:
pattern = f"^{re.escape(pattern)}$" # dots and other metacharacters are now literal
match = res.hostnameThis is a one-call addition with no breaking change for correctly written configurations. Users who need real regular-expression behaviour should supply a compiled pattern (re.compile(r"s\.glbimg\.com")), which is already handled by the existing isinstance(pattern, Pattern) branch and is unaffected by this change.
The ALLOWED_SOURCES docstring in config.py was also updated to document the two-mode behaviour explicitly.
Text from GitHub Advisory Database
Affected versions 53
- thumbor, pip<= 7.7.7GitHub
- thumboraffected: < 7.8.0CVE List
- thumbor, PyPIfrom first release, fixed in 7.8.0OSV
- thumbor, PyPI4.1.3OSV
- thumbor, PyPI4.10.0OSV
- thumbor, PyPI4.10.1OSV
- thumbor, PyPI4.10.2OSV
- thumbor, PyPI4.10.3OSV
- thumbor, PyPI4.11.0OSV
- thumbor, PyPI4.11.1OSV
- thumbor, PyPI4.12.0OSV
- thumbor, PyPI4.12.1OSV
- thumbor, PyPI4.12.2OSV
- thumbor, PyPI4.4.1OSV
- thumbor, PyPI4.5.3OSV
- thumbor, PyPI4.5.4OSV
- thumbor, PyPI4.6.0OSV
- thumbor, PyPI4.7.0OSV
- thumbor, PyPI4.7.1OSV
- thumbor, PyPI4.8.0OSV
- thumbor, PyPI4.8.1OSV
- thumbor, PyPI4.8.2OSV
- thumbor, PyPI4.8.3OSV
- thumbor, PyPI4.8.4OSV
- thumbor, PyPI4.8.5OSV
- thumbor, PyPI4.8.6OSV
- thumbor, PyPI4.9.0OSV
- thumbor, PyPI4.9.1OSV
- thumbor, PyPI5.0.0OSV
- thumbor, PyPI5.0.0rc1OSV
- thumbor, PyPI5.0.0rc2OSV
- thumbor, PyPI5.0.1OSV
- thumbor, PyPI5.0.2OSV
- thumbor, PyPI5.0.3OSV
- thumbor, PyPI5.0.4OSV
- thumbor, PyPI5.0.5OSV
- thumbor, PyPI5.0.6OSV
- thumbor, PyPI5.1.0OSV
- thumbor, PyPI5.2.0OSV
- thumbor, PyPI5.2.1OSV
- thumbor, PyPI6.0.0OSV
- thumbor, PyPI6.0.0b1OSV
- thumbor, PyPI6.0.0b2OSV
- thumbor, PyPI6.0.0b3OSV
- thumbor, PyPI6.0.0b4OSV
- thumbor, PyPI6.0.0b5OSV
- thumbor, PyPI6.0.1OSV
- thumbor, PyPI6.0.2OSV
- thumbor, PyPI6.1.0OSV
- thumbor, PyPI6.1.1OSV
- thumbor, PyPI6.1.2OSV
- thumbor, PyPI6.1.3OSV
- thumbor, PyPI6.1.4OSV