SQL injection remains one of the most dangerous and most common vulnerabilities affecting PHP applications in 2026. Despite being well documented for over two decades, our security audits at Coding4 still uncover SQLi flaws in production codebases every single month. The good news: preventing SQL injection in PHP is straightforward once you know the right patterns.
This practical guide walks you through 7 proven techniques to defend your PHP applications, with side-by-side vulnerable vs secure code examples so you can immediately spot and fix the issues in your own projects.
What Is SQL Injection in PHP?
SQL injection (SQLi) happens when an attacker is able to inject malicious SQL fragments into a query because user-supplied data is concatenated directly into the SQL string. The attacker can then read sensitive data, modify records, drop tables, or even gain remote code execution depending on database privileges.
Here is the textbook example of a vulnerable PHP query:
// VULNERABLE - never do this
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);
If a user submits ' OR '1'='1 as the username, the resulting query becomes SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '', which authenticates the attacker without any valid credentials.
Let’s fix this for good.

1. Use PDO With Prepared Statements (The Gold Standard)
According to the official PHP manual and every security authority, the recommended way to prevent SQL injection is by binding all data via prepared statements. PDO (PHP Data Objects) gives you a clean, database-agnostic API to do exactly that.
Vulnerable Code
$id = $_GET['id'];
$pdo->query("SELECT * FROM products WHERE id = $id");
Secure Code With PDO
$dsn = 'mysql:host=localhost;dbname=shop;charset=utf8mb4';
$pdo = new PDO($dsn, $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' => $_GET['id']]);
$product = $stmt->fetch();
The critical detail: set PDO::ATTR_EMULATE_PREPARES to false. With emulation enabled, PDO builds the query string itself, which weakens the protection. With it disabled, the query and parameters are sent separately to the database engine, making injection mathematically impossible.
2. Use MySQLi Prepared Statements
If your project relies on the MySQLi extension, the principle is identical: separate the query structure from the data.
Vulnerable Code
$email = $_POST['email'];
$result = mysqli_query($conn, "SELECT id FROM users WHERE email = '$email'");
Secure Code With MySQLi
$stmt = $conn->prepare('SELECT id FROM users WHERE email = ?');
$stmt->bind_param('s', $_POST['email']);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
The type indicator string passed to bind_param() follows this mapping:
| Type | Meaning |
|---|---|
| s | String |
| i | Integer |
| d | Double / float |
| b | Blob (sent in packets) |
3. Validate and Sanitize User Input
Prepared statements protect against injection, but input validation is your second line of defense and helps reject obviously malformed data before it ever reaches the database.
- Cast numeric IDs explicitly:
$id = (int) $_GET['id']; - Use
filter_var()for emails, URLs, IPs, and booleans - Use allow-lists (whitelists) for values from a fixed set (e.g. sort order)
- Reject input that exceeds expected length
Example: Validating a Sort Column
// Column names cannot be bound as parameters, so use a whitelist
$allowedSort = ['name', 'price', 'created_at'];
$sort = in_array($_GET['sort'] ?? '', $allowedSort, true) ? $_GET['sort'] : 'name';
$stmt = $pdo->prepare("SELECT * FROM products ORDER BY $sort LIMIT :limit");
$stmt->bindValue(':limit', (int) ($_GET['limit'] ?? 20), PDO::PARAM_INT);
$stmt->execute();

4. Stop Using mysql_real_escape_string() as Your Only Defense
The legacy mysql_* extension was removed back in PHP 7.0, but we still see codebases relying on mysqli_real_escape_string() as the sole protection. Escaping is fragile: it depends on the correct connection charset, it only works inside quoted strings, and it cannot protect identifiers or numeric contexts.
Why Escaping Alone Fails
// Looks safe, but it's NOT
$id = mysqli_real_escape_string($conn, $_GET['id']);
$sql = "SELECT * FROM users WHERE id = $id"; // unquoted numeric context
// Attacker submits: 1 UNION SELECT password FROM admins
// Escaping does nothing because there are no quotes to escape
The fix is the same as always: use a prepared statement. Treat escaping as a legacy fallback only.
5. Use an ORM or Query Builder
Modern PHP frameworks ship with battle-tested ORMs and query builders that produce parameterized SQL by default. If you are starting a new project in 2026, leverage them.
Eloquent (Laravel)
// Safe - values are bound automatically
$user = User::where('email', $request->input('email'))->first();
// Also safe with the query builder
$products = DB::table('products')
->where('category_id', $categoryId)
->where('price', '<', $maxPrice)
->get();
Doctrine DBAL
$qb = $conn->createQueryBuilder();
$qb->select('*')
->from('orders')
->where('user_id = :uid')
->setParameter('uid', $userId);
$orders = $qb->executeQuery()->fetchAllAssociative();
Warning: ORMs are not magic. Raw query methods like DB::raw() or whereRaw() bypass parameter binding. Always pass user data through bindings, never through string concatenation.
// VULNERABLE even in Laravel
DB::select("SELECT * FROM users WHERE name = '".$request->name."'");
// SECURE
DB::select('SELECT * FROM users WHERE name = ?', [$request->name]);
6. Apply the Principle of Least Privilege
Even with bulletproof query code, you should harden the database account itself. If the worst happens, you want to limit the blast radius.
- Create a dedicated database user per application
- Grant only the minimum required privileges (
SELECT,INSERT,UPDATE,DELETEon specific tables) - Never run your app with
rootorDBAcredentials - Use a separate read-only account for reporting and analytics
- Disable
FILE,EXECUTE, andSUPERprivileges unless strictly necessary
-- Example: minimal MySQL user for a web app
CREATE USER 'webapp'@'10.0.%' IDENTIFIED BY 'strong_password_here';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'webapp'@'10.0.%';
FLUSH PRIVILEGES;

7. Add a WAF and Continuous Security Testing
Defense in depth means your application is protected even if a single layer fails. In 2026 there is no excuse for skipping these:
- Web Application Firewall (WAF): Cloudflare, AWS WAF, or ModSecurity can block known SQLi payloads before they reach PHP
- Static analysis: Tools like Psalm, PHPStan with security plugins, or Snyk Code catch dangerous patterns in CI
- Dependency scanning:
composer auditis built in since Composer 2.4; run it in every pipeline - DAST scanners: Run Acunetix, Invicti, or OWASP ZAP against staging regularly
- Logging and alerting: Log failed queries and anomalous request patterns to a SIEM
Quick Reference: Vulnerable vs Secure Patterns
| Context | Vulnerable | Secure |
|---|---|---|
| Numeric ID | "WHERE id = $id" |
bindValue(':id', $id, PDO::PARAM_INT) |
| String value | "WHERE name = '$name'" |
Prepared statement with placeholder |
| LIKE search | "LIKE '%$q%'" |
LIKE :q with '%'.$q.'%' bound |
| IN clause | "IN ($csv)" |
Generate ?,?,? placeholders dynamically |
| Column / table name | String concatenation | Whitelist of allowed identifiers |
Bonus: Handling Dynamic IN Clauses Safely
A frequent question: how do you bind a variable number of values? Generate the placeholders programmatically.
$ids = [4, 7, 12, 23]; // from user input, already validated as integers
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
$stmt->execute($ids);
$rows = $stmt->fetchAll();
FAQ
Which function is used to prevent SQL injection in PHP?
There is no single function. The recommended approach is to use PDO or MySQLi prepared statements with bound parameters. Functions like mysqli_real_escape_string() exist but should never be your only defense.
Can SQL injection be completely prevented?
Yes. When every query is built using parameterized prepared statements and dynamic identifiers are validated through allow-lists, SQL injection becomes impossible. Combine this with least-privilege database accounts and a WAF for defense in depth.
Are prepared statements enough on their own?
For data values, yes. But prepared statements cannot bind table or column names. Whenever those need to be dynamic, validate them against a hardcoded whitelist before injecting them into the query string.
Is PDO better than MySQLi for preventing SQL injection?
Both prevent SQL injection equally well when used with real prepared statements. PDO has the advantage of supporting 12+ database drivers and a cleaner API with named placeholders, which is why most modern PHP teams prefer it.
What about addslashes() or htmlspecialchars()?
addslashes() is not a defense against SQL injection. htmlspecialchars() protects against XSS, not SQLi. Neither replaces prepared statements.
Final Thoughts
Preventing SQL injection in PHP comes down to one core principle: never mix user input with SQL syntax. Use prepared statements everywhere, validate input, restrict database privileges, and add automated security testing to your CI/CD pipeline. Follow the 7 techniques above and your PHP application will be immune to one of the oldest and still most exploited vulnerabilities on the web.
Need help auditing your PHP codebase or modernizing legacy queries? The team at Coding4 can review your application and harden it against SQL injection and other OWASP Top 10 threats. Get in touch for a security audit.

