SQL injection is one of the oldest and most dangerous web application vulnerabilities. Despite being well-understood and easily preventable, it continues to appear in production applications, causing data breaches and financial losses. In this guide, I'll show you how to protect your applications from SQL injection attacks.
Understanding SQL Injection
SQL injection occurs when an attacker is able to insert malicious SQL code into a query that your application sends to the database. This happens when user input is concatenated directly into SQL strings without proper sanitization or parameterization.
A Classic Example
A classic example is a login form that builds a query like this:
SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'
If an attacker enters ' OR '1'='1 as the username, the query becomes:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = ''
This query returns all users in the database because '1'='1' is always true. The attacker can log in as any user without knowing the password.
The Consequences
The consequences of SQL injection go far beyond bypassing authentication. Attackers can:
- Read sensitive data (credit card numbers, personal information)
- Modify or delete data
- Execute administrative operations
- In some cases, gain access to the underlying server
SQL injection consistently appears on the OWASP Top 10 list of web application vulnerabilities for good reason. It's common, it's dangerous, and it's entirely preventable.
Use Parameterized Queries
The most effective defense against SQL injection is to use parameterized queries, also called prepared statements. Parameterized queries separate SQL code from data, so user input is always treated as data, never as executable code.
How Parameterized Queries Work
Here is how parameterized queries work in different languages:
# Python with psycopg2
cursor.execute(
"SELECT * FROM users WHERE username = %s AND password = %s",
(username, password)
)
// Node.js with pg
const result = await client.query(
'SELECT * FROM users WHERE username = $1 AND password = $2',
[username, password]
)
// Java with JDBC
PreparedStatement stmt = connection.prepareStatement(
"SELECT * FROM users WHERE username = ? AND password = ?"
);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
In each case, the database driver handles escaping and quoting automatically. Even if the user input contains malicious SQL, it is treated as a string value, not as part of the query.
Avoid Dynamic SQL
Dynamic SQL, where query strings are built by concatenating strings, should be avoided whenever possible. If you must use dynamic SQL, be extremely careful about what you concatenate.
The most dangerous form of dynamic SQL is concatenating user input directly into the query string. Never do this. If you need to build queries dynamically based on user input, use an ORM or query builder that handles parameterization for you.
Sometimes you need to dynamically specify table names or column names, which cannot be parameterized. In these cases, validate the input against a whitelist of allowed values. Never accept arbitrary table or column names from user input.
Input validation is a useful secondary defense against SQL injection. Validate that input matches the expected format before using it in any database operation. For example, if you expect a numeric ID, validate that the input is actually a number.
function getUserById(id) {
// Validate that id is a positive integer
if (!Number.isInteger(id) || id <= 0) {
throw new Error('Invalid user ID')
}
return db.query('SELECT * FROM users WHERE id = $1', [id])
}
Input validation should not replace parameterized queries, but it adds an extra layer of defense. If a bug in your parameterization code somehow allows injection, input validation can still catch the attack.
Apply Least Privilege
The principle of least privilege states that your application should have only the database permissions it needs to function. If your application only needs to read and write data in specific tables, do not connect to the database with an account that can create tables or drop databases.
Create separate database accounts for different parts of your application. The web application might have read and write access to specific tables, while the admin interface might have broader permissions. This limits the damage if an account is compromised.
Use an ORM
Object-Relational Mapping (ORM) libraries like Sequelize, Prisma, SQLAlchemy, and Hibernate handle query parameterization automatically. When you use an ORM's query methods, you get parameterized queries by default without having to think about it.
// Using Prisma
const user = await prisma.user.findUnique({
where: { email: userInput }
})
This is much safer than writing raw SQL queries, and it also makes your code more readable and maintainable. However, be careful with ORM features that allow raw queries, as they can bypass the ORM's safety measures.
Keep Your Database Updated
Database vendors regularly release security updates that fix vulnerabilities. Keep your database software up to date to protect against known vulnerabilities. This includes both the database server and client libraries.
Test for SQL Injection
Include SQL injection testing in your security testing process. Use automated scanners to test your application for SQL injection vulnerabilities. Manual penetration testing by security experts can also identify vulnerabilities that automated tools miss.
Write unit tests that verify your database access code handles malicious input safely. Test with inputs that include SQL metacharacters, quotes, and common injection patterns.
Frequently Asked Questions
What's the difference between SQL injection and XSS?
SQL injection attacks the database by inserting malicious SQL code. XSS (Cross-Site Scripting) attacks users by injecting malicious JavaScript into web pages. Both are serious vulnerabilities, but they target different parts of your application.
Can prepared statements prevent all SQL injection?
Prepared statements prevent SQL injection in most cases, but they're not a silver bullet. You still need to be careful with dynamic SQL, and you should validate input as an additional layer of defense.
What about NoSQL databases?
NoSQL databases like MongoDB can also be vulnerable to injection attacks, though the attack vectors are different. Always validate and sanitize input, regardless of the database you're using.
How do I know if my application has SQL injection vulnerabilities?
Use automated security scanners, conduct manual penetration testing, and review your code for instances where user input is concatenated into SQL queries. Tools like SQLMap can help identify vulnerabilities.
What should I do if I discover SQL injection in my application?
Fix it immediately using parameterized queries. If the vulnerability has been exploited, audit your database for unauthorized access or data modification. Notify affected users if necessary.
The Bottom Line
Preventing SQL injection is straightforward when you follow the right practices. Use parameterized queries for all database operations, avoid dynamic SQL, validate input as a secondary defense, apply least privilege, use an ORM when possible, keep your database updated, and test for vulnerabilities regularly. These practices will protect your applications from one of the most common and dangerous security vulnerabilities.
Remember: security is not optional. One SQL injection vulnerability can lead to a complete data breach. Take the time to do it right, and your applications will be much more secure.