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.
thumbor/loaders/http_loader.py — validate()
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
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 True
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_SOURCES contains at least one plain-string entry (the common case; all official documentation examples use plain strings).7.8.0Exploitability
AV:NAC:LPR:NUI:NScope
S:UImpact
C:HI:NA:L8.2/CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:LResource Management