All vulnerabilities
CVE-2026-53500GHSA-6x26-6r6f-m537PYSEC-2026-3617

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

thumbor thumborpipLibraries & packages
CVSS
8.2High
ExploitationPublic exploit codeCISA's assessment says proof of concept code is public, assessed 31 Jul 2026. Not in the KEV catalogCISA via CVE List
Exploit odds (EPSS)0.50%Chance of exploitation activity in the next 30 days, higher than 41% of CVEs. Scored 23 Oct 2026FIRST EPSS
Fixed in7.8.0Fixed versionGitHub
Published31 Jul 2026Updated 30 Sep 2026CVE List

What fixes it

  • 7.8.0thumbor, pipGitHub
  • 7.8.0thumbor, PyPIOSVpip install "thumbor>=7.8.0"

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 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:

  • ALLOWED_SOURCES contains 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.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.

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

References 4