iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

ORMs Done Right

ORMs (Prisma, Drizzle, SQLAlchemy, Eloquent, ActiveRecord, EF Core, Hibernate) parameterise queries by default. They’re the safest path to SQL injection-free apps — but they all have raw-SQL escape hatches that re-open the hole.

Safe by default, risky escape hatches

EXAMPLE
// 1) Prisma — typed query builder, parameterised by construction
await prisma.user.findMany({
    where: {
        email: { contains: search },                   // safe
        active: true,
    },
    orderBy: { createdAt: 'desc' },
    take: 20,
});

// Raw queries — tagged templates are SAFE
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
// → SELECT * FROM users WHERE email = $1

// UNSAFE — $queryRawUnsafe takes a string + values
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);  // INJECTION
await prisma.$queryRawUnsafe('SELECT * FROM users WHERE email = ?', email);   // SAFE

// 2) Drizzle — similar contract
import { sql, eq } from 'drizzle-orm';
import { users } from './schema';

await db.select().from(users).where(eq(users.email, email));         // safe
await db.execute(sql`SELECT * FROM users WHERE email = ${email}`);  // safe — sql tagged template
// Avoid sql.raw() with user data

// 3) SQLAlchemy (Python)
from sqlalchemy import text, select
from sqlalchemy.orm import Session
from .models import User

with Session(engine) as s:
    # ORM API — safe
    users = s.scalars(select(User).where(User.email == email)).all()

    # Core text() with bound params — safe
    rows = s.execute(
        text('SELECT * FROM users WHERE email = :email'),
        {'email': email},
    ).all()

    # UNSAFE — f-string into SQL
    bad = s.execute(text(f"SELECT * FROM users WHERE email = '{email}'"))

# 4) Eloquent (Laravel)
User::where('email', $email)->first();                 // safe
DB::select('SELECT * FROM users WHERE email = ?', [$email]);   // safe
DB::select("SELECT * FROM users WHERE email = '$email'");      // INJECTION

# 5) ActiveRecord (Rails)
User.where(email: email).first             # safe
User.where('email = ?', email).first       # safe
User.where("email = '#{email}'").first      # INJECTION

# Don't use string interpolation in where clauses; Rails will warn you.

# 6) Entity Framework (C#)
await db.Users.Where(u => u.Email == email).FirstOrDefaultAsync();          // safe
await db.Users.FromSqlInterpolated($"SELECT * FROM Users WHERE Email = {email}")  // safe — interpolated
                                                                                  // becomes parameterised
await db.Users.FromSqlRaw($"SELECT * FROM Users WHERE Email = '{email}'")        // INJECTION
await db.Users.FromSqlRaw("SELECT * FROM Users WHERE Email = {0}", email)         // safe

# 7) Hibernate / JPA (Java)
entityManager.createQuery(
    "FROM User WHERE email = :email", User.class)
    .setParameter("email", email)
    .getSingleResult();                  // safe

// Bad: building HQL with concatenation
// session.createQuery("FROM User WHERE email = '" + email + "'")    // INJECTION

# 8) Dynamic identifiers — even ORMs can't parameterise table/column names
// Always allowlist these — see the allowlist lesson
const SORTS = { id: 'id', created: 'createdAt', total: 'total' };
const sort = SORTS[req.query.sort] ?? 'createdAt';
await prisma.order.findMany({ orderBy: { [sort]: 'desc' } });

# 9) Best practices
#   • Default to the typed query builder — it can't generate SQLi
#   • If you need raw SQL: tagged template or bound parameters, never concatenation
#   • Ban `$queryRawUnsafe` / `FromSqlRaw with interpolation` / `String#format` via lint rules
#   • Restrict DB user permissions (no DROP, no extension installation)
#   • Log + alert on errors with SQL syntax errors (someone might be probing)

# 10) Detect ORM bypasses in code review
# Look for: f-strings, template literals, string concatenation, sprintf with %s where the param is user-controlled
# Look for: raw / FromSqlRaw / queryRawUnsafe / executeRaw / DB::statement / find_by_sql
# Allowed: tagged templates, named params (:foo), positional params (?), bound parameters

Why it matters

ORMs make SQLi the exception, not the rule — but every ORM has a raw-SQL escape hatch. Ban the unsafe ones with a lint rule; require code review on every legitimate raw query.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Good
User::where('email', $email)->first();      // Laravel Eloquent — bound
User.objects.filter(email=email).first()    // Django ORM — bound
// Bad — raw query with concatenation
User::query()->whereRaw("email = '$email'")  // STOP — bind values instead
Try it Yourself »

Exercise

Safe Eloquent call to find by email.

User:: ('email', $email)->first();

Test yourself

Q1. Calling Eloquent's whereRaw with concatenation is…
Q2. A common safe pattern is…
Q3. For dynamic ORDER BY columns, prefer…

Discussion

Loading…