Defeating the Ghost in the Machine: Preventing SQL Injection in TypeORM
I’ll never forget the “Black Tuesday” of my early career. I was working for a FinTech startup in New York, and we were using a popular ORM. I felt untouchable—after all, ORMs handle the messy SQL stuff, right? Wrong. A clever attacker exploited a single, lazily written search filter, dumped our user table, and I spent the next 72 hours on a diet of cold espresso and regret.
That’s the thing about SQL Injection (SQLi): it’s the “classic” vulnerability that just won’t die. Even in 2026, with sophisticated tools at our disposal, it remains a top threat in the OWASP Top 10. Today, I’m going to show you why using TypeORM doesn’t make you bulletproof and how to write code that actually sleeps well at night.
The Myth of ORM Invincibility
There’s a dangerous misconception among mid-level devs that an Object-Relational Mapper (ORM) is a magic security shield. The logic goes: “I’m writing TypeScript, not SQL, so I’m safe.”
In reality, an ORM is just a translator. If you give it “dirty” instructions, it will faithfully translate those into “dirty” SQL. SQL Injection happens when untrusted user input is treated as code instead of data. When you bypass the ORM’s built-in parameterization, you’re essentially handing the keys of your database to anyone with a browser.
TypeORM’s Built-in Protections
To be fair, TypeORM is excellent when used as intended. When you use standard repository methods like findOneBy(), TypeORM uses parameterized queries (also known as prepared statements).
Instead of sending a giant string to the database, it sends the query structure and the data separately. The database engine never “executes” the data part, rendering injection impossible.
TypeScript
// This is SAFE. TypeORM handles the escaping.
const user = await userRepository.findOneBy({ email: userInputEmail });
Where We (Devs) Mess It Up
The trouble starts when we need to do something “complex.” We reach for the QueryBuilder or raw SQL, and that’s where the “battle scars” come from. Here are the two most common ways I see developers accidentally invite hackers to the party.
1. The QueryBuilder Concatenation Trap
I’ve seen this in dozens of PRs. A developer wants to build a dynamic search filter and thinks they’re being efficient by using string interpolation.
Vulnerable Code:
TypeScript
// DANGER: Never do this!
const users = await userRepository
.createQueryBuilder("user")
.where(`user.name = '${userInput}'`) // Direct string interpolation
.getMany();
If userInput is ' OR 1=1 --, the resulting SQL becomes SELECT * FROM users WHERE user.name = '' OR 1=1 --'. Suddenly, the attacker sees every user in your system.
2. The “Raw Query” Shortcut
Sometimes we get frustrated with TypeORM’s abstraction and just want to run a quick SQL command.
Vulnerable Code:
TypeScript
// DANGER: This is a direct injection vector
const rawData = await dataSource.query(
`SELECT * FROM products WHERE category = '${req.query.category}'`
);
By bypassing the QueryBuilder without manual sanitization, you’ve removed every layer of protection provided by the library.
The Secure Way: Best Practices
The fix is simpler than most people think. You must always use the named parameters or the object-binding syntax provided by TypeORM.
The Correct QueryBuilder Approach
Instead of template literals, use the colon syntax (:value) and pass an object as the second argument.
TypeScript
// SECURE: TypeORM parameterizes the 'name' variable
const users = await userRepository
.createQueryBuilder("user")
.where("user.name = :name", { name: userInput })
.getMany();
The Correct Raw Query Approach
If you must use .query(), use the second argument to pass an array of parameters.
TypeScript
// SECURE: The database driver handles the binding
const rawData = await dataSource.query(
"SELECT * FROM products WHERE category = $1",
[req.query.category]
);
Input Validation: The Unsung Hero
In my experience, the best way to prevent a database breach is to ensure malicious data never even reaches your ORM. Security is about defense-in-depth.
I strongly recommend using a validation schema library like Zod or class-validator. If you’re expecting a UUID and the user sends a SQL snippet, your application should reject it at the controller level before the DB even knows it exists.
TypeScript
import { z } from 'zod';
const SearchSchema = z.object({
category: z.string().min(1).max(50).alphanum(), // Strictly allow only alphanumeric
});
// Validate early, fail fast
const validatedInput = SearchSchema.parse(req.query);
Conclusion: Review Your Queries Today
TypeORM is a powerful ally, but it isn’t a substitute for a security-first mindset. SQL Injection is a preventable tragedy. If there is one takeaway from my “battle scars,” it’s this: Treat every single piece of user input as a potential exploit.
Take thirty minutes today to grep your codebase for .where(\`` or .query(“. If you find string interpolation inside your database calls, fix it. Your future self (and your users) will thank you.
Happy—and secure—coding!

