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.
- L15Prepared statements
- 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.
- 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.
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.
// 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.
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.
Select the patterns you would use in production. The simulator scores you on the patterns that are both selected and genuinely safe.
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.
Three rules for raw queries
- Use the ORM's safe raw method. Prisma's
Prisma.sql, Drizzle'ssqltemplate tag, ActiveRecord's positionalwhere('col = ?', val). These methods tag the value as a parameter at the byte level. - 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.
- 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.
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 correct1.Why is Prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}` vulnerable to SQLi?
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.Why is the raw + escape pattern (escape the user value, then concatenate) fragile?