How to Prevent SQL Injection
What you’ll build or solve
You’ll secure database queries by separating user input from SQL logic.
When this approach works best
This approach works best when:
Learn SQL on Mimo
- You accept user input in forms such as login or search.
- You build APIs that generate SQL from request parameters.
- You use dynamic queries in backend code.
Skip this only if your system never accepts external input, which is uncommon in real applications.
Prerequisites
- Basic understanding of SQL queries
- A backend language such as JavaScript, Python, PHP, or Java
- Access to your database driver or ORM
Step-by-step instructions
Step 1: Replace string concatenation with parameterized queries
SQL injection happens when you build queries by inserting raw user input into a string.
Unsafe example:
JavaScript
const query = "SELECT * FROM users WHERE email = '" + email + "'";
If a user submits:
' OR 1=1 --
The query becomes:
SQL
SELECT * FROM users WHERE email = '' OR 1=1 --'
The condition is bypassed.
Instead, use parameterized queries. They keep SQL structure separate from data.
Option A: JavaScript with Node.js
JavaScript
const query = "SELECT * FROM users WHERE email = ?";
db.query(query, [email]);
Option B: Python with psycopg2
Python
cursor.execute(
"SELECT * FROM users WHERE email = %s",
(email,)
)
Option C: PHP with PDO
PHP
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
In all cases, placeholders such as ? or %s prevent user input from changing SQL logic.
What to look for
- Your SQL contains placeholders, not concatenated variables.
- User values are passed separately as parameters.
- No user input appears directly inside quotes in the query string.
This is the primary defense against SQL injection.
Step 2: Whitelist dynamic identifiers
Placeholders protect values, not table names or column names.
Unsafe:
JavaScript
const query = `SELECT * FROM ${tableName}`;
A malicious value could inject additional SQL.
Instead, restrict identifiers to a predefined list:
JavaScript
const allowedTables = ["users", "orders"];
if (!allowedTables.includes(tableName)) {
throw new Error("Invalid table");
}
const query = `SELECT * FROM ${tableName}`;
Only allow known, approved identifiers.
Use the same pattern for sorting fields:
JavaScript
const allowedFields = ["name", "created_at"];
if (!allowedFields.includes(sortField)) {
throw new Error("Invalid sort field");
}
const query = `SELECT * FROM users ORDER BY ${sortField}`;
This prevents structural manipulation of the query.
Step 3: Add defense-in-depth layers
Parameterized queries are the core protection. The following measures reduce risk further.
Validate input on the server
Validation improves security and data quality.
JavaScript:
JavaScript
if (!email.includes("@")) {
throw new Error("Invalid email");
}
Python:
Python
if not user_id.isdigit():
raise ValueError("Invalid ID")
Validation does not replace parameterization. It reduces unexpected input and logic errors.
Use least-privilege database accounts
Even if injection succeeds, limit damage by restricting permissions.
Example in MySQL:
SQL
GRANT SELECT, INSERT, UPDATE
ON database_name.*
TO 'app_user'@'localhost';
Avoid using administrative accounts in application code.
Limited privileges prevent attackers from dropping tables or modifying schema.
Examples you can copy
Example 1: Secure login query in Node.js
JavaScript
const query = "SELECT id FROM users WHERE email = ? AND password = ?";
db.query(query, [email, password]);
User input cannot modify query structure.
Example 2: Secure user lookup in Python
Python
cursor.execute(
"SELECT id, name FROM users WHERE id = %s",
(user_id,)
)
The driver treats user_id strictly as data.
Example 3: Secure dynamic sorting
JavaScript
const allowedFields = ["name", "created_at"];
if (!allowedFields.includes(sortField)) {
throw new Error("Invalid sort field");
}
const query = `SELECT * FROM users ORDER BY ${sortField}`;
The sorting column is restricted to approved values.
Example 4: Combined protection pattern
JavaScript
if (!email.includes("@")) {
throw new Error("Invalid email");
}
const query = "SELECT * FROM users WHERE email = ?";
db.query(query, [email]);
Validation plus parameterization provides layered protection.
Common mistakes and how to fix them
Mistake 1: Escaping instead of parameterizing
You might manually escape quotes:
JavaScript
email = email.replace("'", "\\'");
const query = "SELECT * FROM users WHERE email = '" + email + "'";
Why it breaks:
Escaping rules vary by database and are easy to miss.
Correct approach:
JavaScript
const query = "SELECT * FROM users WHERE email = ?";
db.query(query, [email]);
Always use prepared statements.
Mistake 2: Trusting client-side validation
You might validate input only in the browser.
Why it breaks:
Attackers can bypass frontend checks using API tools.
Correct approach:
Validate and parameterize on the server side.
Mistake 3: Using high-privilege database credentials
You might connect using a full admin account.
Why it breaks:
If injection occurs, attackers can alter or delete data.
Correct approach:
SQL
GRANT SELECT, INSERT, UPDATE
ON database_name.*
TO 'app_user'@'localhost';
Grant only required permissions.
Troubleshooting
If injection still works, confirm every query uses parameterized statements.
If placeholders do not work, verify that your database driver supports prepared statements.
If dynamic identifiers are required, confirm they come from a strict whitelist.
If login queries fail after refactoring, check parameter order matches placeholder order.
If errors mention %s or ?, confirm you are using the correct placeholder style for your database driver.
Quick recap
- Never concatenate user input into SQL strings.
- Use parameterized queries as the primary defense.
- Whitelist dynamic table and column names.
- Validate input on the server side.
- Use least-privilege database accounts.
- Layer protections to reduce risk and impact.
Join 40M+ people learning for free on Mimo
4.8 out of 5 across 1M+ reviews
Check us out on Apple AppStore, Google Play Store, and Trustpilot