Question
If user input is inserted directly into an SQL query without being handled safely, the application becomes vulnerable to SQL injection.
For example:
$unsafe_variable = $_POST['user_input'];
mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");
In this case, a malicious user could submit input such as:
value'); DROP TABLE table;--
That could turn the query into something like:
INSERT INTO `table` (`column`) VALUES ('value'); DROP TABLE table;--')
How can this be prevented in PHP?
Short Answer
By the end of this page, you will understand what SQL injection is, why directly concatenating user input into SQL is dangerous, and how to prevent it in PHP using prepared statements, parameterized queries, and basic validation. You will also see common mistakes, real-world usage patterns, and a small practical project.
Concept
SQL injection happens when untrusted input is treated as part of an SQL command instead of as plain data.
In unsafe code, a query is built by joining strings:
$sql = "INSERT INTO users (name) VALUES ('" . $_POST['name'] . "')";
If the input contains SQL syntax such as quotes, comments, or extra commands, the database may interpret that input as executable SQL.
The core protection is prepared statements.
With prepared statements, you send the SQL structure separately from the data. The database knows which parts are commands and which parts are values. That means user input is handled as data only, not as SQL code.
Safe code looks like this:
$stmt = $pdo->prepare("INSERT INTO users (name) VALUES (?)");
$stmt->execute([$_POST['name']]);
Why this matters in real programming:
- It protects login forms, search forms, filters, and admin tools.
- It prevents attackers from reading, changing, or deleting data.
- It avoids fragile string-escaping logic.
- It is the standard secure way to interact with databases in PHP.
Important note: the old mysql_* functions are deprecated and removed. In modern PHP, use PDO or MySQLi.
Mental Model
Think of SQL as a form with blanks to fill in.
- The SQL query is the printed form.
- User input is the value written into the blanks.
If you build SQL by concatenating strings, you are letting the user write directly on the form, including in the instruction area. They can change the meaning of the whole form.
Prepared statements work like locked fields:
- The structure of the form is fixed first.
- The user can only provide values for specific blanks.
- Their input cannot rewrite the instructions.
So the rule is simple: users provide data, not SQL syntax.
Syntax and Examples
Safe syntax with PDO
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'username', 'password');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->prepare("INSERT INTO users (name) VALUES (:name)");
$stmt->execute([
':name' => $_POST['user_input']
]);
Why this is safe
:nameis a placeholder.- The SQL statement is prepared first.
- The value from
$_POST['user_input']is sent separately. - Even if the input contains quotes or SQL-like text, it is treated as data.
Safe syntax with MySQLi
$mysqli = new mysqli('localhost', 'username', 'password', 'test');
$stmt = $mysqli->prepare("INSERT INTO users (name) VALUES (?)");
$stmt->bind_param('s', $_POST['user_input']);
$stmt->execute();
Meaning of 's'
In bind_param('s', ...), the s means the value is a string.
Common types:
s= stringi= integerd= doubleb= blob
Example with a SELECT query
Unsafe:
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
Safe with PDO:
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $_GET['id']]);
$user = $stmt->fetch();
Input validation still matters
Prepared statements prevent injection, but validation is still useful.
For example, if id must be an integer:
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false) {
exit('Invalid id');
}
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);
Validation checks whether the data is acceptable for your business rules. Prepared statements make the SQL safe.
Step by Step Execution
Consider this PDO example:
$name = "value'); DROP TABLE users;--";
$stmt = $pdo->prepare("INSERT INTO users (name) VALUES (:name)");
$stmt->execute([':name' => $name]);
Step by step
-
$nameis assigned a suspicious-looking string. -
$pdo->prepare(...)sends the SQL structure to the database:INSERT INTO users (name) VALUES (:name) -
The database sees
:nameas a value placeholder, not as part of the SQL command. -
$stmt->execute([':name' => $name])sends the input separately. -
The database stores the entire text:
value'); DROP TABLE users;-- -
No extra SQL command is executed.
Compare with unsafe concatenation
$name = "value'); DROP TABLE users;--";
$sql = "INSERT INTO users (name) VALUES ('$name')";
Now the input is mixed directly into the SQL string. The database may interpret parts of it as SQL syntax. That is the vulnerability.
Key takeaway
The same input can be either:
- harmless data with prepared statements
- dangerous SQL when concatenated into a query string
Real World Use Cases
Prepared statements are used anywhere an application sends external input to a database.
Common examples
- Login forms: checking email and password records safely
- Registration forms: inserting usernames, emails, and profile data
- Search filters: querying products, users, or orders by user-provided terms
- Admin dashboards: updating records based on form submissions
- REST APIs: reading route parameters and request body values safely
- Import scripts: inserting CSV or JSON data into tables
Example: login lookup
$stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE email = :email");
$stmt->execute([':email' => $_POST['email']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
Example: product search
$stmt = $pdo->prepare("SELECT * FROM products WHERE category_id = :category_id");
$stmt->execute([':category_id' => $_GET['category_id']]);
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
Example: updating a row
$stmt = $pdo->prepare("UPDATE users SET display_name = :name WHERE id = :id");
$stmt->execute([
':name' => $_POST['display_name'],
':id' => $_POST['id']
]);
In all of these cases, the pattern is the same: never merge raw input into SQL strings.
Real Codebase Usage
In real PHP projects, developers usually combine prepared statements with a few other habits.
1. Validation before querying
Check that the input matches expectations.
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false) {
throw new InvalidArgumentException('Invalid user id');
}
2. Guard clauses
Stop early when required input is missing or invalid.
$email = trim($_POST['email'] ?? '');
if ($email === '') {
exit('Email is required');
}
3. Centralized database access
Many projects create one PDO connection and reuse it through repository or service classes.
function findUserByEmail(PDO $pdo, string $email): array|false {
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute([':email' => $email]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
4. Error handling with exceptions
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
This makes database problems easier to catch and debug safely.
5. Whitelisting for SQL identifiers
Prepared statements protect values, but not table names or column names. If a column name is dynamic, developers usually whitelist allowed values.
$allowedSortColumns = ['name', 'created_at'];
$sort = $_GET['sort'] ?? 'name';
if (!in_array($sort, $allowedSortColumns, true)) {
$sort = 'name';
}
$sql = "SELECT * FROM users ORDER BY $sort";
Here, whitelisting is needed because placeholders cannot usually replace identifiers like column names.
6. Secure authentication flow
For passwords, real projects use password_hash() and password_verify() rather than trying to compare plain text in SQL.
Common Mistakes
1. Using string concatenation
Broken:
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";
Why it is wrong:
- Raw input is inserted directly into the query.
- Quotes inside input can break the SQL structure.
Fix:
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute([':email' => $_POST['email']]);
2. Relying only on escaping
Broken idea:
$email = addslashes($_POST['email']);
$sql = "SELECT * FROM users WHERE email = '$email'";
Why it is wrong:
addslashes()is not a safe SQL injection defense.- Escaping rules depend on the database connection and encoding.
Fix:
- Use prepared statements.
3. Using removed mysql_* functions
Broken:
mysql_query($sql);
Why it is wrong:
mysql_*is obsolete and removed from modern PHP.- It encourages outdated coding patterns.
Fix:
- Use PDO or MySQLi.
4. Assuming numeric input is always safe
Broken:
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
Why it is risky:
- Even expected numeric input should not be trusted blindly.
- Validation and parameter binding should still be used.
Fix:
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false) {
exit('Invalid id');
}
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);
5. Trying to bind table names or column names
Broken idea:
$stmt = $pdo->prepare("SELECT * FROM users ORDER BY :column");
Why it is wrong:
- Placeholders are for values, not SQL keywords, table names, or column names.
Fix:
- Use a whitelist of allowed identifiers.
6. Forgetting business-rule validation
Prepared statements stop SQL injection, but they do not ensure the value is meaningful.
Example:
- An age of
-500 - An empty email
- A username that is too long
You still need validation for correctness.
Comparisons
| Approach | Safe from SQL injection? | Recommended? | Notes |
|---|---|---|---|
| String concatenation | No | No | User input becomes part of SQL |
addslashes() | No | No | Not a reliable SQL defense |
mysqli_real_escape_string() | Better but not ideal | Usually no for new code | Safer than manual escaping, but prepared statements are better |
| Prepared statements with PDO | Yes | Yes | Clean, flexible, widely used |
| Prepared statements with MySQLi | Yes | Yes | Good option for MySQL specifically |
PDO vs MySQLi
| Feature | PDO | MySQLi |
|---|---|---|
| Supports multiple databases | Yes | No, MySQL only |
| Prepared statements | Yes | Yes |
| Named placeholders | Yes | No |
| Positional placeholders | Yes | Yes |
| Common beginner choice | Yes | Yes |
Validation vs prepared statements
| Purpose | Validation | Prepared statements |
|---|---|---|
| Checks if input is acceptable | Yes | No |
| Prevents SQL injection | No | Yes |
| Example | FILTER_VALIDATE_INT | prepare() + execute() |
You usually need both: validation for correctness and prepared statements for security.
Cheat Sheet
Safe rule
Never put raw user input directly into SQL strings.
Use PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute([':email' => $email]);
Use MySQLi
$stmt = $mysqli->prepare("SELECT * FROM users WHERE email = ?");
$stmt->bind_param('s', $email);
$stmt->execute();
Good practices
- Use prepared statements for all external input
- Use PDO or MySQLi, not
mysql_* - Validate input with
filter_input()or custom checks - Use whitelists for dynamic column names or sort directions
- Enable PDO exceptions for easier debugging
Important edge cases
- Placeholders work for values, not table names or column names
- Prepared statements do not replace business validation
- Escaping functions are not the preferred modern solution
Unsafe example
$sql = "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'";
Safe example
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_POST['name']]);
FAQ
Is addslashes() enough to prevent SQL injection?
No. It is not a reliable SQL injection defense. Use prepared statements instead.
Should I still validate input if I use prepared statements?
Yes. Prepared statements make queries safe, but validation checks whether the input is acceptable for your application.
Is PDO better than MySQLi?
Both support prepared statements and are safe when used correctly. PDO is more flexible because it supports multiple database systems.
Can prepared statements protect table names and column names?
No. Placeholders are for values only. For dynamic identifiers, use a whitelist.
Are old mysql_query() functions safe to use?
No. The mysql_* extension is obsolete and removed. Use PDO or MySQLi.
Do prepared statements stop all security problems?
No. They prevent SQL injection when used properly, but you still need validation, authentication, authorization, and proper error handling.
What about numeric values like IDs?
They should still be validated and bound as parameters. Do not trust request data just because it looks numeric.
Mini Project
Description
Build a small PHP form that saves a comment into a MySQL database safely. This demonstrates the most common real-world fix for SQL injection: using PDO prepared statements instead of string concatenation.
Goal
Create a comment submission script that accepts user input, validates it, and inserts it into the database without exposing the application to SQL injection.
Requirements
- Create a PDO database connection with error mode enabled
- Read a comment from a POST request
- Reject empty comments
- Insert the comment using a prepared statement
- Show a success message when the insert works
Keep learning
Related questions
Are PDO Prepared Statements Enough to Prevent SQL Injection in PHP?
Learn how PDO prepared statements prevent SQL injection in PHP, what they protect, and the mistakes that still leave MySQL apps vulnerable.
Can You Bind an Array to an IN Clause in PHP PDO?
Learn how PDO handles placeholders in IN() clauses, why arrays cannot be bound directly, and the safe PHP pattern to build dynamic queries.
Choosing the Right MySQL Collation for PHP and UTF-8
Learn how MySQL character sets and collations work with PHP, and how to choose a practical UTF-8 setup for web applications.