Skip to Content
Course content

85: Writing Prepared Statements Safely

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've sat through countless code reviews where a developer points to a block of code and says, "I'm using prepared statements, so this is 100% injection-proof." They feel a sense of security because they've swapped $stmt->query() for $stmt->prepare(). But here is the cold, hard truth: prepared statements are not a magic shield that makes any variable safe to drop into a query. They only protect you when you are binding values.

The "Prepared Statements Stop All Injection" Myth

The most dangerous misconception I see is the belief that you can use placeholders (? or :name) for anything in your SQL string. Let's look at a common scenario: you want to let a user choose which column to sort their search results by.

// WARNING: This will NOT work and is a common mistake
$sortColumn = $_GET['sort']; // User sends 'price' or 'created_at'
$sql = "SELECT * FROM products ORDER BY :column DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute(['column' => $sortColumn]);

If you run this, you'll likely get a result, but it won't be sorted. Why? Because the database driver treats the bound parameter as a string literal. The database sees ORDER BY 'price' DESC. Sorting by a static string constant does nothing. Now, when the developer realizes the placeholder doesn't work, they often "fix" it by doing this:

// DANGER: This is a textbook SQL injection vulnerability
$sortColumn = $_GET['sort']; 
$sql = "SELECT * FROM products ORDER BY $sortColumn DESC"; 
$stmt = $pdo->prepare($sql); // Preparing a string that is already tainted!
$stmt->execute();

By concatenating the variable directly into the query string, you've just handed the keys to your database to anyone with a browser. The fact that you called prepare() afterwards is irrelevant because the malicious payload is already baked into the SQL command before it ever hits the prepare method.

Separating Data from Structure

To write these safely, you have to understand the boundary between data and structure. Prepared statements are designed to protect data (the values in a WHERE clause, the values being INSERTED). They cannot be used for structure—things like table names, column names, or SQL keywords (ASC/DESC).

If you need a dynamic column name, you cannot bind it. You have to handle it in your PHP logic before the query is built. I always tell my juniors: if it's not a value, it doesn't belong in a placeholder.

Hardening Dynamic Identifiers with Allow-lists

So, how do you actually solve the sorting problem without opening a security hole? You use an allow-list. You define exactly which inputs are acceptable, and if the user provides anything else, you either throw an error or fall back to a safe default. I prefer the default approach because it prevents the app from crashing just because a user messed up a URL parameter.

$allowedSorts = ['price', 'created_at', 'rating'];
$userInput = $_GET['sort'] ?? 'created_at';

// If the input isn't in our list, force it to the default
$sortColumn = in_array($userInput, $allowedSorts) ? $userInput : 'created_at';

// Now it is safe to concatenate because $sortColumn is guaranteed 
// to be one of our three hard-coded strings.
$sql = "SELECT * FROM products ORDER BY $sortColumn DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute();

By using in_array(), you've created a strict gate. The user can send 'price; DROP TABLE users;--' all they want, but your code will simply see that it's not in the allow-list and reset it to 'created_at'. This is the only way to handle dynamic identifiers safely.




📋 Practical Task

Build a Secure Dynamic Product Filter

You are building a product gallery where users can filter items by a specific category and sort them by different criteria. You need to implement this using a mix of prepared statement placeholders for the data and an allow-list for the structure.

Requirements:

  • Create a PHP script that accepts two $_GET parameters: category and sort_by.
  • The category should be handled via a prepared statement placeholder (? or :category) since it is a value.
  • The sort_by parameter must be validated against an allow-list containing 'price', 'name', and 'stock_level'. If the input is invalid or missing, default it to 'name'.
  • Write the final PDO code to execute the query: SELECT * FROM products WHERE category = [category] ORDER BY [sort_by] ASC.

Goal: Ensure that no matter what is passed into the URL, the query cannot be manipulated to access other tables or bypass the category filter.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.