Ormar ORM SQL Injection via min()/max() Aggregate Methods (CVE-2026-26198)
by Sergi Cortés (sergicortesabadia) · 2026-07-05
- Severity
- Critical
- CVE
- CVE-2026-26198 (GHSA-xxh2-68g9-8jqr)
- Category
- web
- Affected product
- Ormar (async Python ORM, commonly used with FastAPI/Starlette)
- Affected versions
- 0.9.9 through 0.22.0 (fixed in 0.23.0)
- Disclosed
- 2026-07-05
- Patch status
- patched
Archive entry
intelseclab/poc-archiveMetadata
| Field | Value |
|---|---|
| Date Added | 2026-07-05 |
| Last Updated | 2026-03 |
| Author / Researcher | Sergi Cortés (sergicortesabadia) |
| CVE / Advisory | CVE-2026-26198 (GHSA-xxh2-68g9-8jqr) |
| Category | web |
| Severity | Critical |
| CVSS Score | 9.8 (CVSSv3, per source repository) |
| Status | PoC |
| Tags | sql-injection, python, ormar, orm, fastapi, sqlalchemy, cwe-89, unauthenticated |
| Related | N/A |
Affected Target
| Field | Value |
|---|---|
| Software / System | Ormar (async Python ORM, commonly used with FastAPI/Starlette) |
| Versions Affected | 0.9.9 through 0.22.0 (fixed in 0.23.0) |
| Language / Platform | Python 3, FastAPI/SQLAlchemy-based applications using Ormar |
| Authentication Required | No |
| Network Access Required | Yes (any application endpoint that exposes an Ormar min()/max() aggregate call with attacker-influenced column input) |
Summary
CVE-2026-26198 is a SQL injection vulnerability in the Ormar async ORM’s min() and max() aggregate query methods. While the sibling sum() and avg() methods validate that the supplied “column” parameter refers to an actual numeric field on the model, min() and max() skip validation entirely because they can legitimately operate on non-numeric fields (strings, dates) — but the fix for that removed all validation rather than just the numeric-type check. As a result, a string such as (SELECT password FROM users LIMIT 1) passed as the “column” argument flows unmodified into sqlalchemy.text(), which treats it as literal SQL, letting an attacker exfiltrate arbitrary data via a crafted subquery. The repository provides a runnable, self-contained reproduction: a minimal vulnerable FastAPI/Ormar app, an exploit demo script, a patched version showing the whitelist-based fix, and tests proving both the vulnerability and the fix.
Vulnerability Details
Root Cause
SelectAction.get_text_clause() in Ormar builds a raw sqlalchemy.text() expression directly from the user-supplied column string for min()/max() calls without validating that the string is an actual, existing model field name, unlike the parallel sum()/avg() code path which does perform that check.
Attack Vector
- Application exposes (directly or indirectly) an endpoint that calls
Model.objects.max(column)or.min(column)with a column value influenced by user input. - Attacker supplies a payload such as
(SELECT password FROM users LIMIT 1)instead of a legitimate column name. - Ormar passes this string unchanged into
sqlalchemy.text(), which is wrapped in the aggregate SQL function (e.g.max((SELECT password FROM users LIMIT 1))) and executed against the database. - The query result (e.g. an admin password hash) is returned to the attacker through the application’s normal response path for the aggregate value.
Impact
Unauthenticated, database-level SQL injection allowing arbitrary data exfiltration (and potentially further SQL-driven abuse) from any application built on a vulnerable Ormar version that exposes min()/max() with attacker-controlled column parameters.
Environment / Lab Setup
Target: Python 3 app using ormar 0.9.9-0.22.0 with FastAPI/SQLAlchemy, SQLite (no external DB required for the demo)
Attacker: Python 3, pip install -r requirements.txt (pytest, ormar, fastapi, etc.)
Proof of Concept
PoC Script
See
vulnerable_app.py,exploit_demo.py,patched_app.py,test_vulnerability.py, androot_cause.mdin this folder.
| |
vulnerable_app.py defines a minimal FastAPI/Ormar model and endpoint using the vulnerable max()/min() pattern; exploit_demo.py runs an interactive demonstration that injects a subquery as the “column” argument and shows the resulting data leak against a local SQLite database; test_vulnerability.py contains automated tests proving the injection works against the vulnerable app and is blocked by patched_app.py’s whitelist-based validation.
Detection & Indicators of Compromise
Signs of compromise:
- Unexpected
SELECT/subquery syntax appearing in the “column” parameter of logged Ormar aggregate calls - Anomalous read access to sensitive tables (e.g. users/credentials) correlating with aggregate-endpoint requests
- Application error logs showing SQL syntax errors from malformed injection attempts against
min()/max()endpoints
Remediation
| Action | Detail |
|---|---|
| Primary fix | Upgrade to ormar 0.23.0 or later, which validates all aggregate method column parameters (min, max, sum, avg) against the model’s known field names |
| Interim mitigation | Never pass user-controlled strings directly as the column argument to min()/max(); whitelist-validate any dynamic column selection against the model’s field list before calling these methods |
References
Notes
Mirrored from https://github.com/sergicortesabadia/CVE-2026-26198-analysis on 2026-07-05.
| |