Why SQL Injection Still Haunts PHP Developers in 2026
Despite being one of the oldest attack vectors in web security, SQL injection remains at the top of the OWASP Top 10 threats. Every year, thousands of PHP applications get compromised because a developer concatenated a user-supplied string directly into a SQL query. The good news? Once you understand how the attack works and adopt prepared statements, you can eliminate this entire class of vulnerabilities from your codebase.
This guide walks you through concrete, side-by-side examples of vulnerable PHP code and its secured version, using both PDO and MySQLi. No theory dumps, just real code you can copy, adapt, and ship.

What is SQL Injection? A Quick Refresher
SQL injection happens when an attacker manipulates a SQL query by inserting malicious input into a field that gets concatenated directly into the query string. The database engine cannot distinguish between the developer’s intended SQL and the attacker’s injected SQL, so it executes both.
A Classic Vulnerable Example
Here is a typical login script that any junior developer might write: Source: https://brightsec.com.
<?php
$username = $_POST['username'];
$password = $_POST['password'];
$conn = new mysqli("localhost", "root", "", "myapp");
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = $conn->query($query);
if ($result->num_rows > 0) {
echo "Welcome!";
}
?>
An attacker submits this in the username field:
admin' --
The query becomes:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = ''
The double dash comments out the password check. The attacker is now logged in as admin. Game over.
The Golden Rule: Never Concatenate User Input Into SQL
The single most important principle for preventing SQL injection in PHP is this: separate SQL code from data. Prepared statements do exactly that. The SQL query template is sent to the database first, then the parameters are sent separately and treated strictly as values, never as executable code.

Method 1: Prevent SQL Injection with PDO Prepared Statements
PDO (PHP Data Objects) is the modern, database-agnostic way to talk to your database in PHP. It works with MySQL, PostgreSQL, SQLite, SQL Server, and more.
Vulnerable Code
<?php
$id = $_GET['id'];
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass');
$result = $pdo->query("SELECT * FROM products WHERE id = $id");
foreach ($result as $row) {
print_r($row);
}
?>
Secured Version with PDO
<?php
$id = $_GET['id'];
$pdo = new PDO('mysql:host=localhost;dbname=myapp;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare('SELECT * FROM products WHERE id = :id');
$stmt->execute(['id' => $id]);
$products = $stmt->fetchAll();
foreach ($products as $row) {
print_r($row);
}
?>
Critical PDO Configuration
Notice the three attributes we set on the PDO connection. They are not optional if you care about security:
- PDO::ATTR_EMULATE_PREPARES => false: Forces real prepared statements at the database level instead of PDO simulating them in PHP. This is the single most overlooked setting.
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION: Throws exceptions on errors so you never silently swallow a failure.
- charset=utf8mb4: Prevents obscure encoding-based injection attacks.
Method 2: Prevent SQL Injection with MySQLi Prepared Statements
If you are stuck with MySQLi (legacy projects, existing codebases), you can still write secure code. It just takes more ceremony. sitepoint.com has a solid rundown on this.
Vulnerable Code
<?php
$email = $_POST['email'];
$mysqli = new mysqli("localhost", "user", "pass", "myapp");
$result = $mysqli->query("SELECT id, name FROM users WHERE email = '$email'");
?>
Secured Version with MySQLi
<?php
$email = $_POST['email'];
$mysqli = new mysqli("localhost", "user", "pass", "myapp");
$mysqli->set_charset("utf8mb4");
$stmt = $mysqli->prepare("SELECT id, name FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
print_r($row);
}
$stmt->close();
?>
Understanding bind_param Type Codes
| Code | Type | Example |
|---|---|---|
| s | String | username, email |
| i | Integer | user id, age |
| d | Double / Float | price, rating |
| b | Blob | binary files |
Real-World Scenario: A Secure Login Form
Let’s put everything together with a proper login form using PDO, prepared statements, and password hashing.
<?php
session_start();
$pdo = new PDO('mysql:host=localhost;dbname=myapp;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE username = :username LIMIT 1');
$stmt->execute(['username' => $username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password_hash'])) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
header('Location: /dashboard.php');
exit;
}
$error = 'Invalid credentials';
}
?>

What About Dynamic Table or Column Names?
Prepared statements can only parameterize values, not identifiers. If you need to dynamically choose a column to sort by, never pass user input directly into the query. Use a whitelist instead:
<?php
$allowedColumns = ['name', 'price', 'created_at'];
$sortBy = in_array($_GET['sort'] ?? '', $allowedColumns, true) ? $_GET['sort'] : 'name';
$stmt = $pdo->prepare("SELECT * FROM products ORDER BY $sortBy ASC");
$stmt->execute();
?>
PDO vs MySQLi: Which Should You Use?
| Feature | PDO | MySQLi |
|---|---|---|
| Database support | 12+ drivers | MySQL only |
| Named parameters | Yes | No (positional only) |
| API style | OOP | OOP + procedural |
| Learning curve | Gentle | Verbose |
Our recommendation: Use PDO for new projects. It is cleaner, more portable, and its named parameters make complex queries far more readable.

Additional Defensive Layers
Prepared statements are your primary defense, but a serious application should layer additional protections:
- Validate input: Enforce data types, lengths, and formats before the value ever reaches your database layer.
- Principle of least privilege: Your application’s database user should only have the permissions it actually needs. Never connect as root.
- Use an ORM or query builder: Tools like Doctrine, Eloquent, or the query builder in Laravel wrap prepared statements for you.
- Web Application Firewall (WAF): A WAF like Cloudflare or ModSecurity can catch injection attempts before they hit your app.
- Keep PHP and MySQL updated: Security patches matter. Run supported versions.
- Log and monitor: Failed queries and abnormal patterns are often the first sign of a probe.
Common Mistakes That Still Leave You Vulnerable
- Using addslashes() or mysql_real_escape_string() as your only defense. Escaping is not the same as parameterizing and can be bypassed with clever encoding.
- Leaving PDO::ATTR_EMULATE_PREPARES set to true. With emulation on, PDO builds the query string in PHP, which reintroduces risk in edge cases.
- Concatenating variables into the query string, even after “validating” them. Validation is not a substitute for parameterization.
- Trusting data from cookies, headers, or hidden form fields. Anything the client sends is untrusted.
FAQ: Preventing SQL Injection in PHP
Can SQL injection be completely prevented?
Yes. Using prepared statements with parameter binding for every single query that involves user input eliminates classical SQL injection. Combined with input validation and least-privilege database accounts, the risk drops to near zero.
Is mysql_real_escape_string enough to prevent SQL injection?
No. It has been deprecated since PHP 5.5 and removed in PHP 7. Even in its heyday, it could be bypassed under certain character set conditions. Always use prepared statements.
Which is better for preventing SQL injection: PDO or MySQLi?
Both are safe when used correctly with prepared statements. PDO is more flexible and modern, so we recommend it for new projects. MySQLi remains a solid choice for MySQL-only legacy code.
Do I still need to sanitize input if I use prepared statements?
Prepared statements protect against SQL injection specifically. You still need to validate and sanitize input for other purposes such as preventing XSS on output, enforcing business rules, and rejecting malformed data.
Can prepared statements protect against every kind of injection?
Prepared statements protect the values you bind, not identifiers like table or column names. For dynamic identifiers, use a strict whitelist approach.
What happens if I forget to disable emulated prepares in PDO?
With emulation enabled, PDO constructs the final query string in PHP before sending it to MySQL. In most cases this is still safe, but obscure charset bugs have historically been exploited. Disabling emulation ensures true server-side parameterization.
Wrapping Up
Preventing SQL injection in PHP is not complicated, but it does require discipline. Every query that involves user input must use prepared statements, period. Whether you pick PDO or MySQLi, the pattern is the same: prepare the query with placeholders, bind the values, execute. Do this consistently and you will never ship a SQL injection vulnerability again. There’s a good explainer over at paragonie.com.
At dailygit.com, we help developers build secure, maintainable web applications. Bookmark this guide, share it with your team, and audit your codebase this week. Your future self will thank you.
