high

CVE-2026-53500

PyPI · thumbor

Summary

Thumbor treats ALLOWED_SOURCES string patterns as unescaped regex, allowing hostname bypass via wildcard dot

Severity
high
CVSS
8.2
EPSS
0.5% (p42)
CWE
CWE-1333
Also known as
GHSA-6x26-6r6f-m537
Published
2026-07-31
Updated
2026-07-31

Advisory details

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 True

Impact

An attacker who can influence the image source URL passed to Thumbor can fetch images from arbitrary hosts, bypassing the ALLOWED_SOURCES allowlist.

Preconditions:

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.hostname

This 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.

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.