Skip to content
securitybest-practicesadvanced

SQL Injection Prevention: Every Developer Needs to Know This

8 min readMasterSQL

SQL injection occurs when user input is inserted directly into SQL queries without sanitization. An attacker can manipulate the query to access, modify, or delete data. The only reliable prevention is using parameterized queries (prepared statements). Never concatenate user input into SQL strings.

SQL injection is the oldest web vulnerability in the book. It was first documented in 1998. It is still in the OWASP Top 10 in 2026. Every year, companies get breached because of it. The fix is simple, but developers still make this mistake.

What Is SQL Injection?

SQL injection happens when an attacker inserts malicious SQL code into a query through user input. The database executes the injected code along with the intended query.

Here is a simple example. Suppose you have a login form:

// Vulnerable code (Node.js)
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
db.query(query);

If a user enters admin' -- as the username, the query becomes:

SELECT * FROM users WHERE username = 'admin' --' AND password = ''

The -- comments out the rest of the query. The password check is ignored. The attacker logs in as admin without knowing the password. This is the simplest form of SQL injection, but the damage can be much worse. An attacker can use UNION queries to dump entire tables, modify data, or even drop tables if the database user has broad permissions.

Types of SQL Injection

In-band SQL Injection

The attacker sees the result directly in the application response. This is the most common type.

-- Classic UNION-based injection
' UNION SELECT username, password FROM users --

-- The application shows the injected results alongside the original query

Blind SQL Injection

The attacker does not see the result directly but can infer information from the application's behavior (response time, error messages, different pages for true/false conditions).

-- Time-based blind injection
' OR SLEEP(5) --

-- If the response takes 5 seconds, the injection worked
-- Boolean-based blind injection
' OR 1=1 --  (returns all users)
' OR 1=2 --  (returns no users)

Out-of-band SQL Injection

The attacker extracts data through a different channel (DNS requests, HTTP requests). This is less common but works when in-band injection is not possible.

The Fix: Parameterized Queries

The only reliable way to prevent SQL injection is using parameterized queries (also called prepared statements). The database treats user input as data, not as executable SQL code.

Node.js (mysql2)

// Vulnerable
const query = `SELECT * FROM users WHERE id = ${userId}`;
db.query(query);

// Safe: Parameterized query
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]);

// Safe: Named parameters
const query = 'SELECT * FROM users WHERE id = :id';
db.query(query, { id: userId });

Python (mysql-connector)

# Vulnerable
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")

# Safe: Parameterized query
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))

PHP (PDO)

// Vulnerable
$stmt = $pdo->query("SELECT * FROM users WHERE id = $user_id");

// Safe: Prepared statement
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$user_id]);

Java (JDBC)

// Vulnerable
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE id = " + userId);

// Safe: PreparedStatement
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
pstmt.setInt(1, userId);
ResultSet rs = pstmt.executeQuery();

Additional Defenses

Input Validation

Validate input before it reaches the database. Reject unexpected characters and formats.

// Validate email format
function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

// Validate integer
function isInteger(value) {
  return /^\d+$/.test(value);
}

// Use validation before querying
if (!isInteger(userId)) {
  throw new Error('Invalid user ID');
}

Least Privilege

The database user your application uses should have the minimum permissions needed. Do not use the root user.

-- Create a limited user for the application
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'localhost';

-- The app user CANNOT:
-- DROP TABLE
-- CREATE TABLE
-- ALTER TABLE
-- GRANT permissions

Stored Procedures

Stored procedures can help, but they are not a silver bullet. If the stored procedure itself concatenates input, it is still vulnerable.

-- Safe stored procedure
CREATE PROCEDURE GetUser(IN p_user_id INT)
BEGIN
  SELECT * FROM users WHERE id = p_user_id;
END;

-- The input is treated as a parameter, not concatenated

Web Application Firewall (WAF)

A WAF can detect and block common SQL injection patterns. It is a defense-in-depth measure, not a primary defense. Cloudflare, AWS WAF, and ModSecurity are popular options.

Common Mistakes That Lead to SQL Injection

  1. Concatenating user input into queries - The root cause of all SQL injection
  2. Using string formatting for SQL - f"SELECT * FROM users WHERE id = ${id}" is vulnerable
  3. Trusting input from forms - All user input is untrusted until validated
  4. Using the root database user - If the app is compromised, the attacker has full database access
  5. Not validating input types - An ID field should only accept integers
  6. Displaying raw error messages - Error messages can reveal database structure to attackers

Testing for SQL Injection

Test your application by trying to inject SQL into every input field:

  • Enter a single quote ' - Does it cause a database error?
  • Enter ' OR '1'='1 - Does it return more rows than expected?
  • Enter ' UNION SELECT null, null -- - Does it change the result structure?
  • Enter ' AND SLEEP(5) -- - Does the response take 5 seconds longer?

If any of these produce unexpected results, your application is vulnerable.

Key Takeaways

  • Always use parameterized queries (prepared statements) to prevent SQL injection
  • Never concatenate user input into SQL strings, even for "trusted" internal data
  • Apply defense in depth: parameterized queries plus input validation plus least-privilege database users
  • Test every input field with common injection payloads during development
  • ORMs help but are not bulletproof; raw query modes can still be vulnerable

FAQ

Can ORMs prevent SQL injection?

Most ORMs use parameterized queries by default, which prevents SQL injection. But ORMs often have a "raw query" mode that is vulnerable if you concatenate input. Always check how your ORM handles raw queries.

Are prepared statements 100% safe?

Yes, when used correctly. Prepared statements separate SQL code from data. The database parses the SQL structure first, then binds the data. The data can never change the SQL structure. This is the most reliable way to prevent SQL injection.

What about stored procedures?

Stored procedures help if they use parameters correctly. But if the stored procedure concatenates input internally, it is still vulnerable. Stored procedures are not a substitute for parameterized queries in the application layer.

Can SQL injection happen through headers or cookies?

Yes. Any user-controlled input that ends up in a SQL query is a potential vector. Headers like User-Agent, Referer, and custom headers can all be manipulated. Treat all HTTP input as untrusted.

How do I handle dynamic table or column names?

You cannot use parameterized queries for identifiers (table names, column names). Use a whitelist approach: validate the input against a list of allowed values, and reject any input that does not match. Never construct identifiers from raw user input.

Should I sanitize input by escaping special characters?

No. Escaping is an incomplete defense. Different databases have different escape rules, and it is easy to miss edge cases. Parameterized queries handle escaping automatically and correctly. Use validation for business rules, not for security.

Real-World SQL Injection Attack Scenario

Consider a common attack pattern. An e-commerce search function concatenates user input directly into a query. An attacker enters a payload that extracts email addresses and password hashes from the users table through UNION injection. The data is exfiltrated and used to launch credential stuffing attacks against other services.

The vulnerable code looked like this:

// Vulnerable search endpoint
const searchTerm = req.query.q;
const query = `SELECT name, price FROM products WHERE name LIKE '%${searchTerm}%'`;
db.query(query, (err, results) => {
  res.json(results);
});

The fix was straightforward. The team replaced the string concatenation with a parameterized query and added input validation to reject payloads containing SQL keywords. They also limited the database user permissions so the application account could only read from the products table. The incident response took three days because the team had to audit every query in the codebase to find other vulnerable endpoints.

This type of attack is preventable. Parameterized queries, input validation, and least-privilege database users would have stopped it before any data was exposed. The cost of prevention is minimal compared to the cost of a breach, which includes customer notification, regulatory fines, and reputational damage.

When to Apply Each Defense Layer

Parameterized queries are your first and most important defense. Use them for every query that includes user input, no exceptions. Input validation is your second layer. Validate data types, lengths, and formats before the input reaches the query. A user ID field should only accept integers. An email field should match an email pattern. Reject anything that does not match.

Least-privilege database users are your third layer. If an attacker bypasses the first two layers, a restricted database user limits the damage. The application user should only have the permissions it needs. For a read-only reporting app, grant only SELECT. For a write-heavy app, grant SELECT, INSERT, and UPDATE, but not DROP or ALTER.

Web application firewalls are your fourth layer. They catch patterns that slip past the other defenses. A WAF is not a substitute for secure coding, but it buys time while you patch vulnerabilities. Deploy it as a safety net, not as your primary defense.

M

Written by

MasterSQL

Related Articles

Related Tutorials