VYPR
Critical severityNVD Advisory· Published Feb 24, 2026· Updated Feb 24, 2026

ormar is vulnerable to SQL Injection through aggregate functions min() and max()

CVE-2026-26198

Description

Ormar is a async mini ORM for Python. In versions 0.9.9 through 0.22.0, when performing aggregate queries, Ormar ORM constructs SQL expressions by passing user-supplied column names directly into sqlalchemy.text() without any validation or sanitization. The min() and max() methods in the QuerySet class accept arbitrary string input as the column parameter. While sum() and avg() are partially protected by an is_numeric type check that rejects non-existent fields, min() and max() skip this validation entirely. As a result, an attacker-controlled string is embedded as raw SQL inside the aggregate function call. Any unauthorized user can exploit this vulnerability to read the entire database contents, including tables unrelated to the queried model, by injecting a subquery as the column parameter. Version 0.23.0 contains a patch.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

An SQL injection vulnerability in Ormar ORM's min() and max() aggregate functions allows unauthenticated attackers to read arbitrary database contents via crafted column input.

Ormar, an async mini ORM for Python, is vulnerable to SQL injection in versions 0.9.9 through 0.22.0. The min() and max() methods of the QuerySet class accept arbitrary string input as the column parameter, which is then passed unsanitized into sqlalchemy.text() via the SelectAction.get_text_clause() method. Unlike sum() and avg(), which perform an is_numeric check to reject non-existent fields, min() and max() lack any validation, allowing an attacker-controlled string to be embedded as raw SQL inside aggregate function calls [1][3].

To exploit this vulnerability, an unauthenticated user simply passes a crafted string, such as a subquery, as the column argument to min() or max(). No authentication or special privileges are required; the attacker only needs access to an endpoint that uses these aggregate functions with user-supplied column names. The vulnerable code path has remained unchanged since its introduction in version 0.9.9 [1].

Successful exploitation enables an attacker to read the entire database contents, including data from tables unrelated to the queried model, by injecting arbitrary SQL expressions as subqueries. This can lead to complete data exfiltration and compromise of sensitive information stored in the database [1][3].

The vulnerability is fixed in Ormar version 0.23.0. The fix adds a validation check that verifies the column name exists in the target model's fields; if not, a QueryDefinitionError is raised [4]. Users are strongly advised to upgrade to this patched version.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
ormarPyPI
>= 0.9.9, < 0.23.00.23.0

Affected products

2
  • ormar/ormarllm-fuzzy
    Range: >=0.9.9, <=0.22.0
  • collerek/ormarv5
    Range: >= 0.9.9, < 0.23.0

Patches

1
a03bae14fe01

fix vulnerability by column filtering (#1557)

https://github.com/collerek/ormarcollerekFeb 22, 2026via ghsa
5 files changed · +98 4
  • docs/releases.md+22 2 modified
    @@ -1,8 +1,28 @@
     # Release notes
     
    +## 0.23.0
    +
    +### ‼️🚨 Critical vulnerability fixed – please upgrade ASAP
    +
    +* In this version of ormar the critical vulnerability (`CVE-2026-26198`) in aggregate functions was patched - thanks @AAtomical
    + for reporting. The vulnerability was caused by the way ormar generated SQL queries for aggregate functions, allowing arbitrary SQL execution through user input.
    +* Affected versions:
    +  * `0.9.9 - 0.12.2`
    +  * `0.20.0b1 - 0.22.0 (latest)`
    +
    +### ✨ Breaking changes
    +
    +* Drop support for Python 3.9
    +
    +### 🐛 Fixes
    +
    +* Fix selecting data with nested models with json fields [#1530](https://github.com/collerek/ormar/pull/1530)
    +* Fix prefetching JSON list field throwing TypeError - thanks @jannyware-inc [#1402](https://github.com/collerek/ormar/pull/1402)
    +
    +
     ## 0.22.0
     
    -### 🐛 Breaking changes
    +### ✨ Breaking changes
     
     * **Migration from `databases` library to native async SQLAlchemy**
     
    @@ -122,7 +142,7 @@
     
     ## 0.21.0
     
    -### 🐛 Breaking changes
    +### ✨ Breaking changes
     
     * Drop support for Python 3.8
     * Remove the possibility to exclude parents' fields in children models (discouraged as bad practice anyway)
    
  • ormar/queryset/queryset.py+6 1 modified
    @@ -704,8 +704,13 @@ async def _query_aggr_function(self, func_name: str, columns: List) -> Any:
             if func_name in ["sum", "avg"]:
                 if any(not x.is_numeric for x in select_actions):
                     raise QueryDefinitionError(
    -                    "You can use sum and svg only with" "numeric types of columns"
    +                    "You can use sum and svg only with numeric types of columns"
                     )
    +        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"
    +            )
             select_columns = [x.apply_func(func, use_label=True) for x in select_actions]
             expr = self.build_select_expression().alias(f"subquery_for_{func_name}")
             expr = sqlalchemy.select(*select_columns).select_from(expr)  # type: ignore
    
  • pyproject.toml+1 1 modified
    @@ -3,7 +3,7 @@ name = "ormar"
     
     [tool.poetry]
     name = "ormar"
    -version = "0.22.1"
    +version = "0.23.0"
     description = "An async ORM with fastapi in mind and pydantic validation."
     authors = ["Radosław Drążkiewicz <collerek@gmail.com>"]
     license = "MIT"
    
  • tests/test_vulnerabilities/__init__.py+0 0 added
  • tests/test_vulnerabilities/test_aggregated_functions.py+69 0 added
    @@ -0,0 +1,69 @@
    +from typing import Optional
    +
    +import ormar
    +import pytest
    +
    +from tests.lifespan import init_tests
    +from tests.settings import create_config
    +
    +base_ormar_config = create_config()
    +
    +
    +class Category(ormar.Model):
    +    ormar_config = base_ormar_config.copy(tablename="categories")
    +
    +    id: int = ormar.Integer(primary_key=True)
    +    name: str = ormar.String(max_length=100)
    +
    +
    +class Item(ormar.Model):
    +    ormar_config = base_ormar_config.copy(tablename="items")
    +
    +    id: int = ormar.Integer(primary_key=True)
    +    name: str = ormar.String(max_length=100)
    +    price: float = ormar.Float(default=0)
    +    category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
    +
    +
    +create_test_database = init_tests(base_ormar_config)
    +
    +
    +@pytest.mark.asyncio
    +async def test_arbitrary_sql_execution():
    +    async with base_ormar_config.database:
    +        async with base_ormar_config.database.transaction(force_rollback=True):
    +            if not await Item.objects.count():
    +                cat = await Category.objects.create(name="Electronics")
    +                await Item.objects.create(name="Tablet", price=449.99, category=cat)
    +                await Item.objects.create(name="Monitor", price=329.99, category=cat)
    +
    +            column = "1 + 1"
    +            with pytest.raises(ormar.exceptions.QueryDefinitionError):
    +                await Item.objects.min(column)
    +
    +
    +@pytest.mark.asyncio
    +async def test_arbitrary_sql_execution_on_related_model():
    +    async with base_ormar_config.database:
    +        async with base_ormar_config.database.transaction(force_rollback=True):
    +            if not await Item.objects.count():
    +                cat = await Category.objects.create(name="Electronics")
    +                await Item.objects.create(name="Tablet", price=449.99, category=cat)
    +                await Item.objects.create(name="Monitor", price=329.99, category=cat)
    +
    +            column = "category__1 + 1"
    +            with pytest.raises(ormar.exceptions.QueryDefinitionError):
    +                await Item.objects.min(column)
    +
    +
    +@pytest.mark.asyncio
    +async def test_schema_extraction():
    +    async with base_ormar_config.database:
    +        async with base_ormar_config.database.transaction(force_rollback=True):
    +            if not await Item.objects.count():
    +                cat = await Category.objects.create(name="Electronics")
    +                await Item.objects.create(name="Laptop", price=999.99, category=cat)
    +
    +            column = "(SELECT GROUP_CONCAT(name) FROM sqlite_master WHERE type='table')"
    +            with pytest.raises(ormar.exceptions.QueryDefinitionError):
    +                await Item.objects.min(column)
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.