SQL Injection in ORM Layers
Object-relational mappers generate SQL on behalf of the application. For standard operations, filter values are passed as parameters, not concatenated into the query string. This prevents injection through the standard query API. Injection becomes possible through specific ORM features that construct SQL from strings, accept raw SQL fragments, or allow user input to influence query structure rather than query values.
Raw Query Escapes
Every major ORM provides a mechanism to execute raw SQL for cases the query builder cannot handle. These mechanisms do not apply parameterization automatically; the developer is responsible for it.
Django's RawQuerySet:
# Vulnerable: user input concatenated into raw SQL
User.objects.raw("SELECT * FROM users WHERE name = '" + name + "'")
Laravel's Eloquent whereRaw():
// Vulnerable: no parameter binding
DB::table('users')->whereRaw("name = '" . $name . "'")->get();
Sequelize query() with string interpolation:
// Vulnerable: template literal without replacements
sequelize.query(`SELECT * FROM users WHERE name = '${name}'`);
Each ORM provides parameterized equivalents: Django's raw() accepts a params list; Eloquent's whereRaw() accepts a bindings array; Sequelize's query() accepts a replacements object. The parameterized forms are safe. The string-interpolated forms are not.
Dynamic Column and Table Names
SQL parameters bind values. They cannot bind identifiers: table names, column names, or ORDER BY column references. When an application allows user input to control which column results are sorted by, the column name cannot be parameterized. It must be embedded directly in the SQL string.
# sort_column comes from a request parameter
queryset = queryset.order_by(sort_column)
Django's order_by() accepts field names and passes them into the generated SQL as identifiers. If sort_column is not validated against an allowlist of permitted column names, the user controls a SQL identifier. The injection surface here differs from value injection: the goal is not to close a string literal but to manipulate the identifier context.
The only safe approach for dynamic identifiers is an allowlist. Map user-supplied sort keys to hardcoded column names in application code before passing them to the ORM.
Mass Assignment and Filter Operator Injection
Some ORMs accept filter conditions as dictionaries where keys represent field names and lookup operators. Django's ORM uses double-underscore syntax: User.objects.filter(name__contains=value). If the filter key is constructed from user input:
field = request.GET.get('field')
User.objects.filter(**{field: value})
The user can set field to password__startswith or is_admin__exact, turning a name filter into a password extraction or privilege check. This is not SQL injection in the traditional sense; the ORM still parameterizes the value. But user input is controlling query structure, enabling unauthorized data access.
Filter keys must be validated against an allowlist of fields the application intends to expose as filterable, just as identifiers must be.