PDO in PHP: Does It 100% Protect Against SQL Injections?

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Myths and Reality of PDO

\n\n

If you write PHP and have ever Googled "how to protect against SQL injections," you've likely heard the mantra: "Use PDO and prepared statements — and you'll be safe". This is the gold standard, the foundation of every PHP security tutorial. But is it really true? Is it enough to just write $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?') and forget about the problem?

\n\n

The question "Are PDO prepared statements sufficient to prevent SQL injection?" has garnered thousands of views on Stack Overflow for a reason. The answer, as it turns out, is not so straightforward. In this article, we'll dive under the hood of PDO, break down how prepared statements actually work, and find out in which cases even PDO can let you down.

\n\n

This isn't boring theory. We'll look at real examples, query emulation, encoding nuances, and provide specific action items to make your code truly secure.

\n\n

How Prepared Statements Work: Magic or Math?

\n\n

First, let's recall what an SQL injection is. It's when an attacker passes data into a query that gets interpreted as part of the SQL code. Classic example: ' OR 1=1 --.

\n\n

Prepared statements solve this problem radically. They separate query logic from data. It looks like this:

\n\n
<?php\n$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');\n$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');\n$stmt->execute([':email' => $_GET['email'], ':status' => 'active']);\n?>\n
\n\n

Here's what happens under the hood:

\n\n
    \n
  1. Prepare: The driver sends the database server a query "skeleton": SELECT * FROM users WHERE email = ? AND status = ?. The server parses this skeleton, optimizes it, and remembers the execution plan. The placeholders for data are left empty.
  2. \n
  3. Execute: Now you send only the actual values. The server inserts them into the already prepared plan, but it does so in a special way — it knows these are data, not code. Therefore, any attempt to "close a quote" or add an SQL command simply becomes part of a string literal.
  4. \n
\n\n

Sounds perfect. But there's a catch.

\n\n

The Pitfall: Emulated Prepared Statements

\n\n

The main surprise revealed by that Stack Overflow question is: PDO by default does not always use "real" prepared statements.

\n\n

The issue is that not all databases support this feature at the protocol level. And some drivers (e.g., for SQLite or older MySQL versions) work through emulation. What does that mean?

\n\n

When emulation is enabled, PDO substitutes values into the SQL query on the client side (in PHP) and then sends the already complete query to the server. It does this by escaping special characters using PDO::quote().

\n\n

And this is where problems start. Emulation is essentially the same string substitution, just performed by the library. And if you've set the connection encoding incorrectly, this mechanism can be broken.

\n\n

Example: Attack via Different Encodings

\n\n

A classic attack using incorrect encoding looks like this. Suppose you establish a MySQL connection with the GBK encoding (a Chinese encoding), and the query is emulated. The \\ (backslash) character in GBK can be part of a multi-byte character.

\n\n

An attacker sends the string: %BF%27. In GBK encoding, the sequence %BF%5C forms a single character, while %27 is just a quote. The emulator sees the quote and escapes it, turning %27 into %5C%27 (i.e., \\'). But because

Blogs

Book Recommendations