Login
ChallengesLearn
Scoreboard
Teams
SPNZ

LearnSQL InjectionSQL Injection: ORM security
SQL Injection·Lesson 16 of 20

SQL Injection: ORM security

ORMs help. They also leak. The raw() method, the extra() method, the whereRaw() - every ORM has a footgun and you will use all of them eventually.

Intermediate13 min
SQLiDefenseORM
Loading lesson…
PreviousSQL Injection: Prepared statementsNextSQL Injection: WAF & detection

© 2026 SPNZ.

Terms of ServicePrivacy PolicyCookie Policy

ORMs help. They also leak. Every ORM ships a method that bypasses the safe query builder and lets the developer reach for raw SQL with string interpolation. This lesson catalogues the footguns across Prisma, Drizzle, Sequelize, SQLAlchemy, and Active Record - and the safe alternatives for each.

Prerequisites
Read these lessons first:
  • L15Prepared statements
What you'll be able to do
  • Recognise the raw-SQL footgun method in five common ORMs.
  • Replace each footgun with the safe alternative in the same ORM.
  • Use ORM-native query builders whenever possible; reach for raw queries only when forced.
  • Explain why the "raw + escape" pattern is fragile.
Key terms
Query builder
The fluent API in an ORM that constructs SQL from typed method calls. User values are bound automatically. The safe default.
Raw query
A method that lets the developer write SQL directly. Some raw methods (Prisma.sql, Drizzle.sql) tag values as parameters; others (Sequelize.literal, ActiveRecord's interpolated where) do not.
Footgun
A method whose signature looks safe but produces a string-concatenated query. The most common are Prisma's $queryRaw without Prisma.sql, Sequelize.literal, and ActiveRecord's interpolated where.
Escape-then-bind
The pattern of escaping the user value, then concatenating it into the SQL. Brittle, version-dependent, and bypassable; the only durable fix is a bound parameter.
What is it?

The raw-SQL pattern in five ORMs

Every ORM has the same shape: a query builder that binds values safely, and a raw-SQL escape hatch that the developer reaches for when the builder cannot express the query. The escape hatch is where the leaks live.

javascriptparameterised
// Prisma - VULNERABLE
prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`

// Prisma - SAFE
prisma.$queryRaw`SELECT * FROM users WHERE email = ${Prisma.sql(email)}`

// Drizzle - SAFE
db.select().from(users).where(eq(users.email, sql`${email}`))

// Sequelize - VULNERABLE
Sequelize.literal(`email = '${email}'`)

// SQLAlchemy - VULNERABLE
text(f"SELECT * FROM users WHERE email = '{email}'")

// ActiveRecord - VULNERABLE
User.where("email = '#{params[:email]}'")

// ActiveRecord - SAFE
User.where('email = ?', params[:email])

The rule of thumb: if the SQL string is built with string interpolation (template literals, f-strings, string concatenation), the user value is a token. If the user value travels as a separate argument to a method that the ORM knows is parameterised, it is bound. The raw-SQL methods that look like the second form but behave like the first are the footguns.

The ORM security model
Mini Map
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
Try it

Identify the footguns

Eight code patterns from five ORMs. Select the ones you would use in production. The simulator scores you on the patterns that are both selected and genuinely safe - selecting a footgun costs points.

acme-portalprod
audit modeAadmin
Score
4 / 8

Select the patterns you would use in production. The simulator scores you on the patterns that are both selected and genuinely safe.

Real-world relevance

The pattern across five ORMs

The same SQLi finding has been reported against the same application shape across every major ORM. The Rails blog post that introduces Sequel.literal and warns about it; the Prisma changelog that adds Prisma.sql as the safe alternative; the SQLAlchemy docs that mark text() with a security footnote. The lesson is consistent: the ORM's safe API default protects you 95% of the time, and the remaining 5% is the raw-SQL method that the developer reaches for when the builder is too rigid.

Mitigation

Three rules for raw queries

  1. Use the ORM's safe raw method. Prisma's Prisma.sql, Drizzle's sqltemplate tag, ActiveRecord's positional where('col = ?', val). These methods tag the value as a parameter at the byte level.
  2. Allowlist identifiers. When the user must choose a column, table, or sort name, the identifier is grammar - a bound parameter cannot help. Use a small server-side allowlist.
  3. Lint for the footguns. A Semgrep rule, an ESLint plugin, or a pre-commit grep that flags Sequelize.literal, $queryRaw\`…$${…, and their cousins catches the issue at review time, before the code reaches production.
Further reading
  • Prisma raw queries - Prisma.sql safety(Prisma)
  • Sequelize.literal security warning(Sequelize)
  • SQLAlchemy text() - security note(SQLAlchemy)
Key takeaways

What to remember

  • Every ORM has a raw-SQL method. Some are safe-by-default; some concatenate.
  • The safe pattern: bound parameters in the raw method (Prisma.sql, Drizzle.sql, ActiveRecord positional where).
  • The unsafe pattern: string interpolation, f-strings, or template literals inside the raw method.
  • Lint for the footguns. The most common SQLi findings against ORM-using codebases are the raw-SQL escape hatches.

Knowledge check

0/3 answered · 0 correct
  1. 1.Why is Prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}` vulnerable to SQLi?

  2. 2.The ORM's safe API does not support a query the developer needs (a recursive CTE, a window function, a custom aggregate). What is the right approach?

  3. 3.Why is the raw + escape pattern (escape the user value, then concatenate) fragile?