If you’re building a PHP application that talks to a database, there’s one security flaw you absolutely need to understand: SQL injection. It’s one of the oldest attack techniques on the web, and yet it still ranks at the top of the OWASP list in 2026. The good news? Preventing it is not complicated once you know the right pattern.
In this practical guide, we’ll show you exactly how to prevent SQL injection in PHP using PDO prepared statements and parameterized queries. We’ll put vulnerable code side by side with secure code so you can spot the difference immediately and start writing safer PHP today.
What is SQL Injection?
SQL injection (often shortened as SQLi) is an attack where a malicious user injects SQL code through an input field (like a login form, a search box, or a URL parameter) to manipulate the query executed by your database.
The root cause is always the same: mixing SQL code with user-controlled data. When the two are combined into a single string, the database engine cannot tell where the query logic ends and where the user input begins.
A Quick Real-World Example
Imagine a login form that checks the username and password like this:
// VULNERABLE CODE - DO NOT USE
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);
Now, what happens if an attacker types this into the username field?
admin' --
The resulting SQL query becomes:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = ''
The double dash (--) starts a SQL comment, so the password check is completely ignored. The attacker just logged in as admin without knowing the password. That’s SQL injection in action.

Why Escaping Strings Is Not Enough
Many beginners are told to use mysqli_real_escape_string() or addslashes() to “clean” user input. While these functions offer some protection, they have serious limitations:
- They only handle string escaping, not integer or column name injections.
- Forgetting to call them even once creates a vulnerability.
- They depend on the correct character set being set on the connection.
- They mix data and code, which is the exact problem we want to avoid.
The official PHP manual is clear: the recommended way to prevent SQL injection is to bind all data via prepared statements.

The Right Way: PDO Prepared Statements
Prepared statements separate the SQL query structure from the user data. The database receives the query template first, then the values, so user input can never be interpreted as SQL code. This guide goes deeper on it.
Step 1: Connect to the Database with PDO
<?php
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
'db_user',
'db_password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
exit('Database connection failed.');
}
The important part here is PDO::ATTR_EMULATE_PREPARES => false. This forces real prepared statements at the database level instead of emulated ones in PHP.
Step 2: Rewrite the Vulnerable Login Query
Here is the vulnerable version and the secure version side by side:
| Vulnerable (Do NOT use) | Secure with PDO Prepared Statement |
|---|---|
|
|
Notice two big improvements in the secure version:
- User input is passed as a parameter, never concatenated into the SQL string.
- Passwords are verified with
password_verify()against a hash stored withpassword_hash(), never in plain text.
Step 3: Named vs Positional Parameters
PDO supports two styles of placeholders. Both are safe, pick the one you find more readable.
Named parameters
$stmt = $pdo->prepare(
"INSERT INTO articles (title, content, author_id)
VALUES (:title, :content, :author_id)"
);
$stmt->execute([
':title' => $_POST['title'],
':content' => $_POST['content'],
':author_id' => $_SESSION['user_id'],
]);
Positional parameters (?)
$stmt = $pdo->prepare(
"INSERT INTO articles (title, content, author_id) VALUES (?, ?, ?)"
);
$stmt->execute([
$_POST['title'],
$_POST['content'],
$_SESSION['user_id'],
]);
Real Examples for Common Scenarios
1. Search Form with LIKE
A common mistake is to inject the % wildcards inside the query. Instead, add them to the parameter value:
$search = '%' . $_GET['q'] . '%';
$stmt = $pdo->prepare(
"SELECT id, title FROM products WHERE title LIKE :search LIMIT 20"
);
$stmt->execute([':search' => $search]);
$products = $stmt->fetchAll();
2. Pagination with LIMIT and OFFSET
Integer parameters like LIMIT and OFFSET should be cast explicitly, because emulated prepares can quote them incorrectly:
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 20;
$offset = ($page - 1) * $perPage;
$stmt = $pdo->prepare("SELECT * FROM products LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
3. Dynamic ORDER BY (Column Names Cannot Be Bound!)
Column and table names cannot be parameterized. You must whitelist them:
$allowedSort = ['price', 'title', 'created_at'];
$sort = in_array($_GET['sort'] ?? '', $allowedSort, true)
? $_GET['sort']
: 'created_at';
$dir = ($_GET['dir'] ?? 'desc') === 'asc' ? 'ASC' : 'DESC';
$sql = "SELECT * FROM products ORDER BY $sort $dir LIMIT 50";
$stmt = $pdo->query($sql);

Bonus: Layered Defense Checklist
Prepared statements are your main line of defense, but a secure PHP application applies defense in depth:
- Always use PDO prepared statements (or
mysqliwithbind_param) for every query that touches user input. - Validate input at the application level (types, lengths, allowed values).
- Use least-privilege database accounts. Your web app doesn’t need
DROPorGRANTrights. - Hash passwords with
password_hash()using the default algorithm. - Keep PHP and MySQL/MariaDB updated. In 2026, you should be running PHP 8.3 or 8.4 at minimum.
- Enable a Web Application Firewall (WAF) as an extra safety net, not as a replacement for secure code.
- Log and monitor database errors, but never display them to end users in production.

PDO vs mysqli: Which One Should You Use?
| Feature | PDO | mysqli |
|---|---|---|
| Database support | 12+ drivers | MySQL / MariaDB only |
| Named parameters | Yes | No |
| API style | Object-oriented, consistent | Object-oriented or procedural |
| Prepared statements | Yes | Yes |
For most modern projects, PDO is the recommended choice because it’s portable and its API is cleaner. But if you use mysqli correctly with bind_param(), you are just as safe against injection.
FAQ
Can SQL injection be fully prevented in PHP?
Yes. If you consistently use prepared statements with parameter binding for every query that includes user input, and you whitelist dynamic identifiers like column names, SQL injection becomes practically impossible.
Is mysqli_real_escape_string() safe enough?
No, not on its own. It can help in legacy code, but it’s easy to misuse and doesn’t protect against all injection vectors. Prepared statements are the modern, safe standard.
Do ORMs like Eloquent or Doctrine protect me automatically?
Yes, as long as you use their query builders or ORM methods with parameters. But if you drop down to raw SQL and concatenate strings, you’re back to being vulnerable.
Why should I set PDO::ATTR_EMULATE_PREPARES to false?
By default, PDO may emulate prepared statements in PHP instead of sending them to the database as real prepared statements. Disabling emulation ensures the database engine handles parameter binding, which is safer and gives correct data types.
What about NoSQL databases, are they safe from injection?
They are not immune. NoSQL injection exists too (for example in MongoDB queries built from raw user input). The same principle applies: never mix untrusted input with query structure.
Final Thoughts
SQL injection is a solved problem. The technique to defeat it has existed for over 20 years, and every modern PHP version supports it out of the box. If you take away just one thing from this guide, let it be this:
Never build a SQL query by concatenating user input. Always use PDO prepared statements with bound parameters. Source: https://geeksforgeeks.org.
At PixelFabs, we audit and secure PHP applications every day, and prepared statements are the single most impactful change you can make to protect your users’ data. Start applying this pattern in your next commit, and your future self (and your users) will thank you.