PYSEC-2026-3619
Vulnerability from pysec - Published: 2026-08-04 11:34 - Updated: 2026-08-04 13:36Summary
Thumbor's filters:convolution(<matrix>, <columns>, <should_normalize>) filter passes the user-controlled <columns> value to a C extension (thumbor/ext/filters/_convolution.c) where it is used as a divisor (for % and /) without validating columns > 0. When columns=0, the C code triggers undefined behavior; on x86_64 this reliably results in a fatal divide-by-zero trap (SIGFPE) and crashes the Thumbor process (confirmed on Linux x86_64 and macOS Intel x86_64), causing a remote denial of service.
Details
Root cause
The Python filter accepts columns=0, and the native C extension uses columns_count as a divisor without validating it.
1) Python filter entry point allows columns=0 (thumbor/filters/convolution.py):
@filter_method(
r"(?:[-]?[\d]+\.?[\d]*[;])*(?:[-]?[\d]+\.?[\d]*)",
BaseFilter.PositiveNumber, # accepts 0
BaseFilter.Boolean,
)
async def convolution(self, matrix, columns, should_normalize=True):
...
imgdata = _convolution.apply(..., matrix, columns, should_normalize)
BaseFilter.PositiveNumber matches "0" (thumbor/filters/__init__.py):
class BaseFilter:
PositiveNumber = {"regex": r"[\d]+", "parse": int} # matches "0"
PositiveNonZeroNumber = {"regex": r"[\d]*[1-9][\d]*", "parse": int}
2) C extension divides/modulos by columns_count without a zero check (thumbor/ext/filters/_convolution.c):
kernel_size = PyTuple_Size(kernel_tuple);
if ((kernel_size % columns_count != 0) ||
(kernel_size % 2 == 0) ||
((kernel_size / columns_count) % 2) == 0) {
// TODO: error, not a valid kernel
return NULL;
}
PoC
Test environment
- Linux x86_64
Preconditions
- The
convolutionfilter is enabled (it is enabled by default viaBUILTIN_FILTERS). - Either:
/unsafe/URLs are allowed (ALLOW_UNSAFE_URL=True), OR/unsafe/is disabled, and the attacker has a valid signed URL (i.e., the attacker is an authorized user/partner, or can obtain signed URLs from a trusted signing service).
Example request (signed URL)
http://<host>:<port>/<url-sign>/400x400/filters:convolution(1;2;1;2;4;2;1;2;1,0,true)/example.jpg
Example request (/unsafe/)
http://<host>:<port>/unsafe/400x400/filters:convolution(1;2;1;2;4;2;1;2;1,0,true)/example.jpg
Impact
- Remote Denial of Service via process crash (SIGFPE) on x86_64 (confirmed on Linux x86_64 and macOS Intel x86_64).
- Exploitability depends on deployment:
- If
/unsafe/is enabled: unauthenticated remote DoS. - If
/unsafe/is disabled: the attacker needs a valid signed URL.(i.e., the attacker is an authorized user/partner, or can obtain signed URLs from a trusted signing service)
Suggested remediation
- In the C extension (
thumbor/ext/filters/_convolution.c): - Reject
columns_count <= 0before any%or/. - In the Python filter (
thumbor/filters/convolution.py): - Require
columnsto be non-zero (e.g., useBaseFilter.PositiveNonZeroNumber).
| Name | purl | thumbor | pkg:pypi/thumbor |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "thumbor",
"purl": "pkg:pypi/thumbor"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.8.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"4.1.3",
"4.10.0",
"4.10.1",
"4.10.2",
"4.10.3",
"4.11.0",
"4.11.1",
"4.12.0",
"4.12.1",
"4.12.2",
"4.4.1",
"4.5.3",
"4.5.4",
"4.6.0",
"4.7.0",
"4.7.1",
"4.8.0",
"4.8.1",
"4.8.2",
"4.8.3",
"4.8.4",
"4.8.5",
"4.8.6",
"4.9.0",
"4.9.1",
"5.0.0",
"5.0.0rc1",
"5.0.0rc2",
"5.0.1",
"5.0.2",
"5.0.3",
"5.0.4",
"5.0.5",
"5.0.6",
"5.1.0",
"5.2.0",
"5.2.1",
"6.0.0",
"6.0.0b1",
"6.0.0b2",
"6.0.0b3",
"6.0.0b4",
"6.0.0b5",
"6.0.1",
"6.0.2",
"6.1.0",
"6.1.1",
"6.1.2",
"6.1.3",
"6.1.4",
"6.1.5",
"6.2.0",
"6.2.1",
"6.3.0",
"6.3.1",
"6.3.2",
"6.4.0",
"6.4.1",
"6.4.2",
"6.4.3",
"6.5.0",
"6.5.1",
"6.5.2",
"6.6.0",
"6.6.1",
"6.7.0",
"6.7.1",
"6.7.2",
"6.7.3",
"6.7.4",
"6.7.5",
"6.7.6",
"7.0.0",
"7.0.0a1",
"7.0.0a2",
"7.0.0a3",
"7.0.0a4",
"7.0.0a5",
"7.0.0b1",
"7.0.1",
"7.0.10",
"7.0.11",
"7.0.12",
"7.0.2",
"7.0.3",
"7.0.5",
"7.0.6",
"7.0.7",
"7.0.8",
"7.0.9",
"7.1.0",
"7.1.1",
"7.1.2",
"7.2.0",
"7.2.1",
"7.3.0",
"7.3.1",
"7.3.2",
"7.4.0",
"7.4.1",
"7.4.2",
"7.4.3",
"7.4.4",
"7.4.5",
"7.4.6",
"7.4.7",
"7.5.0",
"7.5.1",
"7.5.2",
"7.6.0",
"7.7.0",
"7.7.1",
"7.7.2",
"7.7.3",
"7.7.4",
"7.7.5",
"7.7.6",
"7.7.7"
]
}
],
"aliases": [
"CVE-2026-53503",
"GHSA-cqjp-jf4r-h5q9"
],
"details": "### Summary\nThumbor\u0027s `filters:convolution(\u003cmatrix\u003e, \u003ccolumns\u003e, \u003cshould_normalize\u003e)` filter passes the user-controlled `\u003ccolumns\u003e` value to a C extension (`thumbor/ext/filters/_convolution.c`) where it is used as a divisor (for `%` and `/`) without validating `columns \u003e 0`. When `columns=0`, the C code triggers undefined behavior; on x86_64 this reliably results in a fatal divide-by-zero trap (SIGFPE) and crashes the Thumbor process (confirmed on Linux x86_64 and macOS Intel x86_64), causing a remote denial of service.\n\n### Details\n#### Root cause\nThe Python filter accepts `columns=0`, and the native C extension uses `columns_count` as a divisor without validating it.\n\n1) Python filter entry point allows `columns=0` (`thumbor/filters/convolution.py`):\n```python\n@filter_method(\n r\"(?:[-]?[\\d]+\\.?[\\d]*[;])*(?:[-]?[\\d]+\\.?[\\d]*)\",\n BaseFilter.PositiveNumber, # accepts 0\n BaseFilter.Boolean,\n)\nasync def convolution(self, matrix, columns, should_normalize=True):\n ...\n imgdata = _convolution.apply(..., matrix, columns, should_normalize)\n```\n`BaseFilter.PositiveNumber` matches `\"0\"` (`thumbor/filters/__init__.py`):\n```python\nclass BaseFilter:\n PositiveNumber = {\"regex\": r\"[\\d]+\", \"parse\": int} # matches \"0\"\n PositiveNonZeroNumber = {\"regex\": r\"[\\d]*[1-9][\\d]*\", \"parse\": int}\n```\n\n2) C extension divides/modulos by `columns_count` without a zero check (`thumbor/ext/filters/_convolution.c`):\n```c\nkernel_size = PyTuple_Size(kernel_tuple);\nif ((kernel_size % columns_count != 0) ||\n (kernel_size % 2 == 0) ||\n ((kernel_size / columns_count) % 2) == 0) {\n // TODO: error, not a valid kernel\n return NULL;\n}\n```\n\n\n### PoC\n#### Test environment\n- Linux x86_64\n\n#### Preconditions\n- The `convolution` filter is enabled (it is enabled by default via `BUILTIN_FILTERS`).\n- Either:\n - `/unsafe/` URLs are allowed (`ALLOW_UNSAFE_URL=True`), OR\n - `/unsafe/` is disabled, and the attacker has a valid signed URL (i.e., the attacker is an authorized user/partner, or can obtain signed URLs from a trusted signing service).\n\n#### Example request (signed URL)\n`http://\u003chost\u003e:\u003cport\u003e/\u003curl-sign\u003e/400x400/filters:convolution(1;2;1;2;4;2;1;2;1,0,true)/example.jpg`\n\n#### Example request (/unsafe/)\n`http://\u003chost\u003e:\u003cport\u003e/unsafe/400x400/filters:convolution(1;2;1;2;4;2;1;2;1,0,true)/example.jpg`\n\n\n### Impact\n- Remote Denial of Service via process crash (SIGFPE) on x86_64 (confirmed on Linux x86_64 and macOS Intel x86_64).\n- Exploitability depends on deployment:\n - If `/unsafe/` is enabled: unauthenticated remote DoS.\n - If `/unsafe/` is disabled: the attacker needs a valid signed URL.(i.e., the attacker is an authorized user/partner, or can obtain signed URLs from a trusted signing service)\n\n\n### Suggested remediation\n- In the C extension (`thumbor/ext/filters/_convolution.c`):\n - Reject `columns_count \u003c= 0` before any `%` or `/`.\n- In the Python filter (`thumbor/filters/convolution.py`):\n - Require `columns` to be non-zero (e.g., use `BaseFilter.PositiveNonZeroNumber`).",
"id": "PYSEC-2026-3619",
"modified": "2026-08-04T13:36:38.176268Z",
"published": "2026-08-04T11:34:46.338544Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/thumbor/thumbor/security/advisories/GHSA-cqjp-jf4r-h5q9"
},
{
"type": "WEB",
"url": "https://github.com/thumbor/thumbor/commit/447e192e3fb92e64c12e5354a56a4a7133f69d73"
},
{
"type": "PACKAGE",
"url": "https://github.com/thumbor/thumbor"
},
{
"type": "WEB",
"url": "https://github.com/thumbor/thumbor/releases/tag/7.8.0"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/thumbor"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-cqjp-jf4r-h5q9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-53503"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "Thumbor convolution filter allows divide-by-zero in C extension leading to remote DoS"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.