GitHub Security Advisory: GHSA-f964-whrq-44h8
Package: ormar (PIP)
Severity: HIGH (CVSS 7.1)
CWE: CWE-20: Improper Input Validation
Vulnerable versions: <= 0.23.0
Patched in: 0.23.1

Summary: ormar Pydantic Validation Bypass via __pk_only__ and __excluded__ Kwargs Injection in Model Constructor

Details:
### Summary

A 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:

```python
# ormar/models/newbasemodel.py, lines 128-142
pk_only = kwargs.pop("__pk_only__", False)      # ← extracted from user kwargs
object.__setattr__(self, "__pk_only__", pk_only)

new_kwargs, through_tmp_dict = self._process_kwargs(kwargs)

if not pk_only:
    # Normal path: full Pydantic validation
    new_kwargs = self.serialize_nested_models_json_fields(new_kwargs)
    self.__pydantic_validator__.validate_python(
        new_kwargs, self_instance=self
    )
else:
    # Bypass path: NO validation at all
    fields_set = {self.ormar_config.pkname}
    values = new_kwargs
    object.__setattr__(self, "__dict__", values)       # raw dict written directly
    object.__setattr__(self, "__pydantic_fields_set__", fields_set)
```

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:

```python
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
    await item.save()
    return item
```

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/pydan