Boost your web app security by converting from inline SQL to prepared statements in PHP and protect against SQL injection vulnerabilities.
Here’s a bug that has burned more developers than almost any other: you drop a variable straight into a SQL string, ship it, and forget about it. Months later someone types the wrong thing into a form field, and your database hands them data you never meant to expose. That’s SQL injection, and it’s still one of the most common ways web apps get breached. The good news is that the fix in PHP is old, boring, and rock solid. It’s called prepared statements.
Let’s walk through what they are, why they beat gluing user input into query strings, and how to convert the queries you already have.
Table of Contents
- What are Prepared Statements?
- The Dangers of Inline SQL
- Why Prepared Statements Are Better
- Migrating from Inline SQL to Prepared Statements
- Conclusion
What are Prepared Statements?
A prepared statement is a query you send to the database in two pieces: the SQL structure first, the user data second. You never mix them into one string. Because the database sees the query shape before it ever sees the values, it already knows which parts are commands and which parts are just data. A value can’t quietly turn into extra SQL, because by the time the data arrives, the query has already been parsed and locked in.
There are two stages to it:
- Preparation: You send the SQL to the database with placeholders where the data will go. The database parses and plans the query, but it doesn’t run it yet.
- Execution: You bind the actual user values to those placeholders, then run the statement.
Here’s the basic shape in PHP using the MySQLi extension:
Example of the code
<?php
// Create a new MySQLi connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Prepare an SQL statement
$stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
// Bind the parameter (i = integer, d = double, s = string, b = blob)
$stmt->bind_param("i", $user_id);
// Execute the prepared statement
$stmt->execute();
$result = $stmt->get_result();
The ? is the placeholder, and bind_param() hands your $user_id to it as pure data. Whatever the value is, the database treats it as a value, never as part of the query. That’s the whole trick.
One honest caveat if you use PDO instead of MySQLi: PDO emulates prepared statements by default, which builds the query string on the PHP side. For true server-side prepares, set PDO::ATTR_EMULATE_PREPARES to false. MySQLi uses real prepared statements out of the box.
The Dangers of Inline SQL
Inline SQL is what happens when you paste user input straight into the query string. It reads fine, it works in testing, and it’s a wide-open door. When the input becomes part of the SQL text, an attacker can send text that the database happily runs as commands.
Here’s the unsafe version:
Example of the code
<?php
$user_id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $user_id";
$result = $conn->query($sql);
Now suppose someone sets the id parameter to 1 OR 1=1. Your query turns into:
SELECT * FROM users WHERE id = 1 OR 1=1;
1=1 is always true, so instead of one user you just handed back the entire table. And that’s the gentle version. The same hole lets an attacker read data they shouldn’t, or with the right payload, change or delete it.
Why Prepared Statements Are Better
Prepared statements shut this down because the input never touches the query structure. You define the SQL, the database plans it, and only then do the values show up, bound as data. There’s no string left for an attacker to break out of. This is the primary defense against SQL injection, and it’s the one OWASP and the PHP manual both point to first.
A few reasons to prefer them over inline SQL:
- Security: This is the big one. User data is never spliced into the query, so there’s nothing to inject into.
- Reusability: Prepare once, execute many times with different values. In a loop, that can save the database from re-parsing the same query over and over.
- Clarity: The query logic and the data stay in separate places, which is just easier to read later.
- Portability: Every modern database supports them, so the pattern travels well between systems.
Migrating from Inline SQL to Prepared Statements
Converting old code is more tedious than hard. The steps:
- Find the queries that use user input: Anywhere a variable gets dropped into a SQL string is a candidate.
- Swap the input for a placeholder: Replace the inline value with a ?.
- Bind the input: Use bind_param() to attach the value to the placeholder.
- Execute: Call execute() to run it, then pull results with get_result() if you need them.
Here’s a typical inline query:
Example of the inline SQL code
<?php
$user_id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $user_id";
$result = $conn->query($sql);And the same thing rewritten safely:
Example of the prepared statement code
<?php
// Create a prepared statement
$stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
// Bind the user input
$stmt->bind_param("i", $user_id);
// Execute the prepared statement
$stmt->execute();
// Fetch the result
$result = $stmt->get_result();
Same result, one important difference: the $user_id now arrives as bound data instead of raw query text. That single change closes the injection hole.
Conclusion
Prepared statements are the practical, well-worn answer to SQL injection in PHP. They work by keeping user input as data instead of letting it become part of your SQL, and that separation is what protects you against one of the oldest bugs on the web.
If you’ve still got inline queries sitting in production, that’s your weekend project. The conversion is mechanical, and you come out of it with safer code that’s also a little cleaner and, in the right spots, a little faster. Your database, and whoever inherits this codebase after you, will thank you.


