LearnContact
Lesson 6024 min read

Prepared Statements

Learn how SQL injection attacks work and how MySQLi prepared statements protect your queries by separating SQL structure from user data.

Introduction

The previous lesson ended with a warning: building SQL queries by directly inserting user input is dangerous. This lesson explains exactly why, and shows you the standard fix that every serious PHP application uses — prepared statements.

By the end of this lesson, you will be able to rewrite any query that touches user input so that it can no longer be hijacked by malicious data.

What You Will Learn
  • What SQL injection is and why string concatenation makes it possible.
  • How prepared statements separate a query's structure from its data.
  • How to use mysqli_prepare(), bind_param(), and execute().
  • How to rewrite a vulnerable query as a safe, prepared one.

What is SQL Injection?

SQL injection happens when untrusted input (typically from a form, a URL, or an API request) is inserted directly into a SQL query string. If that input contains SQL syntax of its own, the database may execute it as part of your query — completely bypassing your intended logic.

This is one of the most well-known and most damaging web application vulnerabilities. It can let an attacker read data they should never see, modify or delete records, or in the worst case, take control of the entire database.

Why This Is Serious

SQL injection has been used in real-world breaches to steal entire user databases, including passwords and personal data. It is entirely preventable, which is exactly why prepared statements are considered mandatory, not optional.

A Vulnerable Example

Imagine a login form that checks a username and password by concatenating them straight into a query.

vulnerable-login.php
<?php
// NEVER do this with real user input
$username = $_POST["username"];
$password = $_POST["password"];

$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = $conn->query($sql);
?>

If an attacker enters `' OR '1'='1` as the username, the query becomes logically true for every row, potentially logging them in as the first user in the table without ever knowing a real password.

resulting-query.sql
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '...'

What Are Prepared Statements?

A prepared statement sends the SQL structure to the database first, with placeholders standing in for actual values, and sends the real data separately afterward. Because the database already knows the query's shape before it ever sees the data, that data can never be reinterpreted as SQL syntax — no matter what characters it contains.

PHP sends the SQL template with ? placeholders to MySQL
MySQL compiles ("prepares") the query structure
PHP sends the actual values separately
MySQL substitutes the values as pure data, never as SQL
The query executes safely
The Core Idea

Prepared statements do not just "escape" dangerous characters — they make it structurally impossible for data to change the meaning of the query, because the query and the data are sent to the database separately.

Prepared Statements with MySQLi

In MySQLi, you prepare a query using `?` as a placeholder for each value, bind real values to those placeholders with `bind_param()`, then run the query with `execute()`.

safe-login.php
<?php
$username = $_POST["username"];
$password = $_POST["password"];

$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();

$result = $stmt->get_result();
$user = $result->fetch_assoc();

$stmt->close();
?>
Reading bind_param()
  • The first argument is a "type string": one letter per placeholder, in order.
  • "s" means string, "i" means integer, "d" means double, "b" means blob.
  • "ss" above means both $username and $password are bound as strings.

Binding Multiple Types

When a query has placeholders of different types, list the type letters in the same order as the values.

bind-types.php
<?php
$stmt = $conn->prepare("INSERT INTO students (name, age, course) VALUES (?, ?, ?)");
$stmt->bind_param("sis", $name, $age, $course);

$name = "Amit Verma";
$age = 21;
$course = "PHP Development";

$stmt->execute();
echo "Rows inserted: " . $stmt->affected_rows;

$stmt->close();
?>
Example Output
Rows inserted: 1

Prepared SELECT, UPDATE & DELETE

The exact same prepare / bind_param / execute pattern applies to every kind of query, not just INSERT.

prepared-update-delete.php
<?php
// UPDATE
$stmt = $conn->prepare("UPDATE students SET course = ? WHERE id = ?");
$stmt->bind_param("si", $newCourse, $id);
$newCourse = "Advanced PHP";
$id = 1;
$stmt->execute();
$stmt->close();

// DELETE
$stmt = $conn->prepare("DELETE FROM students WHERE id = ?");
$stmt->bind_param("i", $id);
$id = 3;
$stmt->execute();
$stmt->close();
?>

Vulnerable vs Safe, Side by Side

Vulnerable (String Concatenation)

php
$sql = "SELECT * FROM users WHERE username = '$username'";
$conn->query($sql);
  • Builds SQL by inserting variables directly into the string.
  • Attacker input can change the meaning of the query.
  • No separation between SQL structure and data.

Safe (Prepared Statement)

php
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
  • Sends the SQL template with placeholders first.
  • Binds real values afterward, strictly as data.
  • User input can never alter the query's structure.

Common Mistakes

Avoid These Mistakes
  • Concatenating any value that came from $_GET, $_POST, or $_COOKIE directly into a query.
  • Getting the order or count of letters in bind_param() wrong compared to the placeholders.
  • Forgetting to close a prepared statement with close() when it is no longer needed.
  • Assuming escaping quotes manually is a safe substitute for prepared statements — it is not.

Best Practices

  • Use prepared statements for every query that includes any value from outside your code.
  • Match bind_param() type letters carefully to each placeholder, in order.
  • Reuse a prepared statement with different bound values when running the same query repeatedly.
  • Never trust "it is just a small internal tool" as a reason to skip prepared statements.

Frequently Asked Questions

Do prepared statements make my application completely secure?

They eliminate SQL injection for the queries that use them, but security also depends on other measures like input validation, password hashing, and proper access control, covered in later lessons.

Is bind_param() required, or can I still use string concatenation carefully?

Prepared statements are the reliable, recommended approach. Manual escaping is error-prone and easy to get wrong, so it should not be relied on as a substitute.

What do the letters in bind_param mean again?

"s" is string, "i" is integer, "d" is double (float), and "b" is blob (binary data) — one letter per placeholder, in the same order as the placeholders appear.

Can prepared statements be used for SELECT, UPDATE, and DELETE too?

Yes. The same prepare, bind_param, execute pattern works identically across INSERT, SELECT, UPDATE, and DELETE statements.

Is PDO also vulnerable to SQL injection without prepared statements?

Yes — the vulnerability comes from concatenating raw input into SQL, regardless of which extension runs the query. PDO has its own prepared statement syntax, covered in the next lesson.

Key Takeaways

  • SQL injection happens when untrusted input is inserted directly into a query string.
  • Prepared statements separate the SQL structure from the actual data, sent to the database separately.
  • MySQLi prepared statements use prepare(), bind_param(), and execute().
  • The bind_param() type string ("s", "i", "d", "b") must match each placeholder in order.
  • Every query touching user input should use a prepared statement, not string concatenation.

Summary

Prepared statements are the standard, reliable defense against SQL injection: the query structure is sent to the database first, and real values are bound afterward as pure data.

In this lesson, you saw how a vulnerable, concatenated query can be exploited, and rewrote it safely using mysqli's prepare(), bind_param(), and execute(). Next, you will meet PDO, an alternative, database-agnostic way of doing all of this.

Lesson 60 Completed
  • You understand how SQL injection exploits string concatenation.
  • You can explain why prepared statements prevent it.
  • You can write MySQLi prepared statements with bind_param() and execute().
  • You are ready to explore PDO as an alternative database layer.
Next Lesson →

PDO (PHP Data Objects)