GHSA-f964-whrq-44h8: ormar Pydantic Validation Bypass via __pk_only__ and __excluded__ Kwargs Injection in Model Constructor
SummaryA Pydantic validation bypass in `ormar`'s model constructor allows any unauthenticated user to skip **all** field validation — type checks, constraints, `@field_validator`/`@model_validator` decorators, choices enforcement, and required-field checks — by injecting `"__pk_only__": true` into a JSON request body. The unvalidated data is subsequently persisted to the database. This affects the **canonical usage pattern** recommended in ormar's official documentation and examples.A secondary `__excluded__` parameter injection uses the same design pattern to selectively nullify arbitrary model fields during construction.Details**Root cause:** `NewBaseModel.__init__` ([`ormar/models/newbasemodel.py`, line 128](https://github.com/collerek/ormar/blob/master/ormar/models/newbasemodel.py#L128)) pops `__pk_only__` directly from user-supplied `**kwargs` before any validation occurs:The `__pk_only__` flag was designed as an internal optimization for creating lightweight FK placeholder instances in [`ormar/fields/foreign_key.py` (lines 41, 527)](https://github.com/collerek/ormar/blob/master/ormar/fields/foreign_key.py#L41). However, because it is extracted from `**kwargs` via `.pop()` with a `False` default, any external caller that passes user-controlled data to the model constructor can inject this flag.**Why the canonical FastAPI + ormar pattern is vulnerable:**Ormar's official example ([`examples/fastapi_quick_start.py`, lines 55-58](https://github.com/collerek/ormar/blob/master/examples/fastapi_quick_start.py#L55)) recommends using ormar models directly as FastAPI request body parameters:FastAPI parses the JSON body and calls `TypeAdapter.validate_python(body_dict)`, which triggers ormar's `__init__`. The `__pk_only__` key is popped at line 128 **before** Pydantic's validator inspects the data, so Pydantic never sees it — even `extra='forbid'` would not prevent this, because the key is already consumed by ormar.The ormar Pydantic `model_config` (set in [`ormar/models/helpers/pydantic.py`, line 108](https://github.com/collerek/ormar/blob/master/ormar/models/helpers/pydantic.py#L108)) does not set `extra='forbid'`, providing no protection even in theory.**What is bypassed when `__pk_only__=True`:**All type coercion and type checking (e.g., string for int field)`max_length` constraints on String fields`choices` constraintsAll `@field_validator` and `@model_validator` decorators`nullable=False` enforcement at the Pydantic levelRequired-field enforcement (only `pkname` is put in `fields_set`)`serialize_nested_models_json_fields()` preprocessing**Save path persists unvalidated data to the database:**After construction with `pk_only=True`, calling `.save()` ([`ormar/models/model.py`, lines 89-107](https://github.com/collerek/ormar/blob/master/ormar/models/model.py#L89)) reads fields directly from `self.__dict__` via `_extract_model_db_fields()`, then executes `table.insert().values(**self_fields)` — persisting the unvalidated data to the database with no re-validation.**Secondary vulnerability — `__excluded__` injection:**The same pattern applies to `__excluded__` at [`ormar/models/newbasemodel.py`, line 292](https://github.com/collerek/ormar/blob/master/ormar/models/newbasemodel.py#L292):At lines 326-329, fields listed in `__excluded__` are silently set to `None`:An attacker can inject `"__excluded__": ["email", "password_hash"]` to nullify arbitrary fields during construction.**Affected entry points:**| Entry Point | Exploitable? |
|---|---|
| `async def create_item(item: Item)` (FastAPI route) | Yes |
| `Model.objects.create(**user_dict)` | Yes |
| `Model(**user_dict)` | Yes |
| `Model.model_validate(user_dict)` | Yes |PoC**Step 1: Create a FastAPI + ormar application using the canonical pattern from ormar's docs:****Step 2: Send a normal request (validation works correctly):****Step 3: Inject `__pk_only__` to bypass ALL validation:****Step 4: Inject `__excluded__` to nullify arbitrary fields:**Impact**Who is impacted:** Every application using ormar's canonical FastAPI integration pattern (`async def endpoint(item: OrmarModel)`) is vulnerable. This is the primary usage pattern documented in ormar's official examples and documentation.**Vulnerability type:** Complete Pydantic validation bypass.**Impact scenarios:****Privilege escalation**: If a model has a `role` or `is_admin` field with a Pydantic validator restricting values to `"user"`, an attacker can set `role="superadmin"` by bypassing the validator**Data integrity violation**: Type constraints (`max_length`, `ge`/`le`, regex patterns) are all bypassed — invalid data is persisted to the database**Business logic bypass**: Custom `@field_validator` and `@model_validator` decorators (e.g., enforcing email format, age ranges, cross-field dependencies) are entirely skipped**Field nullification** (via `__excluded__`): Audit fields, tracking fields, or required business fields can be selectively set to NULL**Suggested fix:**Replace `kwargs.pop("__pk_only__", False)` with a keyword-only parameter that cannot be injected via `**kwargs`:Apply the same fix to `__excluded__`:Internal callers in `foreign_key.py` would pass `_pk_only=True` as a named argument. Keyword-only parameters prefixed with `_` cannot be injected via JSON body deserialization or `Model(**user_dict)` unpacking.
CVSS v3.1: 7.1