Explain SQL Injection and how to prevent it in software applications.
I reviewed a codebase once where every database query was built with template literals. No parameterization. No escaping. The login query looked like this:
14 Mar 2024

I reviewed a codebase once where every database query was built with template literals. No parameterization. No escaping. The login query looked like this:
const query = `SELECT * FROM users WHERE username='${username}' AND password='${password}'`;
An attacker types ' OR 1=1;-- as the username. The query becomes:
SELECT * FROM users WHERE username='' OR 1=1;--' AND password='anything'
The OR 1=1 always evaluates to true. The -- comments out the rest. The attacker is now authenticated as the first user in the table, usually the admin.
That is SQL injection. The application treats user input as code. The database executes it.
Why it is still common
Developers know about SQL injection. It has been in every security guide for 20 years. Yet it still shows up. The reason: convenience. String concatenation is easy. Parameterized queries require slightly more effort. Under deadline pressure, the easy path wins.
Prevention: parameterized queries
Parameterized queries separate the SQL structure from the data. The database knows which parts are code and which parts are values. User input can never change the query logic.
const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
connection.query(query, [username, password], (error, results) => {
// The database treats username and password as literal values
// No SQL structure can be injected
});
This is the primary defense. If you do nothing else, do this.
Prevention: input sanitization
Sanitization is a secondary defense. Remove or escape characters that have special meaning in SQL. This should not be your only protection, but it adds a layer.
function sanitizeInput(input) {
return input.replace(/['";\\]/g, '');
}
const username = sanitizeInput(req.body.username);
The trade-off: sanitization is fragile. Different databases have different special characters. Unicode bypass techniques exist. Rely on parameterized queries first and sanitization as an additional layer.
Prevention: use an ORM
ORMs like Prisma, TypeORM, or Sequelize build queries programmatically. They parameterize by default. You work with objects, not raw SQL strings.
const user = await User.findOne({
where: {
username: req.body.username,
password: req.body.password,
},
});
Benefit: SQL injection is nearly impossible through the ORM's query builder. Less boilerplate. Type safety with TypeScript.
Cost: ORMs generate SQL you do not control. Complex queries can be inefficient. And if you drop down to raw SQL for performance (which you will eventually), you are back to needing parameterized queries.
Prevention: least privilege
Even if an injection succeeds, limit the damage. The database user your application connects with should only have the permissions it needs. Read-only access to tables it only reads. No DROP TABLE privilege. No admin access.
This does not prevent injection. It limits the blast radius.
The bottom line
SQL injection is a solved problem. Parameterized queries eliminate it. The fact that it still appears on the OWASP Top Ten tells you everything about the gap between knowing the fix and consistently applying it.
Use parameterized queries. Use an ORM. Apply least privilege. And never, ever build a query with string concatenation.
Keep reading
- Storing and Handling Sessions in Backend - Interview Question
- Describe the difference between symmetric and asymmetric encryption, and when each is appropriate.
- What is a Buffer Overflow vulnerability, and how can it be exploited?
- What is Cross-Site Scripting (XSS), and how can it be prevented?
- OWASP Top Ten for Software Developers
- API Security Checklist