Introduction: Myths and Reality of PDO
\n\nIf 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?
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\nThis 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\nHow Prepared Statements Work: Magic or Math?
\n\nFirst, 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 --.
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\nHere's what happens under the hood:
\n\n- \n
- 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. \n - 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. \n
Sounds perfect. But there's a catch.
\n\nThe Pitfall: Emulated Prepared Statements
\n\nThe main surprise revealed by that Stack Overflow question is: PDO by default does not always use "real" prepared statements.
\n\nThe 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\nWhen 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().
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\nExample: Attack via Different Encodings
\n\nA 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.
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