
Deep dive into a critical SQL injection in Python's Ormar ORM — reproduction, fix, and tests
A deep dive into a critical (CVSS 9.8) SQL injection vulnerability in a Python async ORM, with reproduction, analysis, and fix.
Ormar is a popular async mini ORM for Python, commonly used with FastAPI and Starlette. Versions 0.9.9 through 0.22.0 contain a SQL injection vulnerability in the min() and max() aggregate methods.
The root cause is a "partial implementation" bug: while sum() and avg() validate that the column parameter refers to an actual numeric field, min() and max() skip this check entirely and pass user input straight into sqlalchemy.text() — a raw SQL sink.
An attacker can inject a subquery as the "column" parameter:
# Expected usage
await Item.objects.max("price") # → SELECT max(price) FROM items
# Attack payload
await Item.objects.max("(SELECT password FROM users LIMIT 1)")
# → SELECT max((SELECT password FROM users LIMIT 1)) FROM items
# Returns the admin's password!
| Attribute | Value |
|---|---|
| CVE ID | CVE-2026-26198 |
| CVSS Score | 9.8 (Critical) |
| CWE | CWE-89: SQL Injection |
| Affected | ormar 0.9.9 – 0.22.0 |
| Fixed in | ormar 0.23.0 |
| Published | February 24, 2026 |
| Auth needed? | None — unauthenticated |
├── README.md ← You are here
├── vulnerable_app.py ← Minimal FastAPI app with the vulnerable pattern
├── exploit_demo.py ← Safe PoC showing the injection in action
├── patched_app.py ← The fixed version with input validation
├── test_vulnerability.py ← Tests proving the vuln exists and the fix works
├── requirements.txt
└── analysis/
└── root_cause.md ← Detailed code-level analysis of the bug
git clone https://github.com/YOUR_USERNAME/CVE-2026-26198-analysis.git
cd CVE-2026-26198-analysis
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# Run the tests (no external DB needed — uses SQLite)
python -m pytest test_vulnerability.py -v
# Run the interactive exploit demo
python exploit_demo.py
The fix validates that the column parameter matches an actual field on the model before it reaches sqlalchemy.text(). This is done through a whitelist approach: only column names that exist in the model's field definitions are allowed.
See patched_app.py for the implementation and analysis/root_cause.md for the full breakdown.
sum()/avg() were validated but min()/max() were not created a false sense of security.MIT