security
SQL Injection
SQL injection occurs when untrusted data changes the structure of a database query. Prepared statements keep query structure separate from parameter values.
What Matters
- Use prepared statements for values supplied to SQL queries.
- Never build SQL by concatenating request values into query text.
- Use allow-lists for identifiers such as sort columns because placeholders bind values, not SQL syntax.
- Use least-privilege database credentials.
- Review raw SQL inside frameworks as carefully as standalone PDO code.
Practical Example
<?php
declare(strict_types=1);
function productQuery(string $search): array
{
return [
'sql' => 'SELECT id, name FROM products WHERE name LIKE :term',
'params' => ['term' => '%' . $search . '%'],
];
}
print_r(productQuery("lamp' OR 1=1 --"));
// Prints:
// [sql] => SELECT id, name FROM products WHERE name LIKE :term
In Application Work
Search boxes, login forms, filters, report builders, imports, and admin tools all create SQL boundaries. The same rule applies to data read from queues or third-party systems.
Bind Values, Allow-List Structure
Prepared-statement placeholders represent values. They cannot safely stand in for table names, column names, or SQL keywords.
<?php
declare(strict_types=1);
function productSearch(string $term, string $requestedSort): array
{
$sortColumns = [
'name' => 'name',
'price' => 'price_cents',
'newest' => 'created_at',
];
if (!isset($sortColumns[$requestedSort])) {
throw new InvalidArgumentException('Sort option is not allowed.');
}
return [
'sql' => 'SELECT id, name FROM products WHERE name LIKE :term ORDER BY ' . $sortColumns[$requestedSort],
'params' => ['term' => '%' . $term . '%'],
];
}
print_r(productSearch('lamp', 'price'));
The search value is bound later. The sort column is selected from code-owned values.
Prepared Statements Are Not Authorisation
A perfectly prepared query can still expose another user's invoice. Apply ownership or permission conditions as part of the query or before returning the record.
Use a database account with only the privileges the application needs. SQL injection is less damaging when the web process cannot create database users, drop unrelated schemas, or access administrative tables.
What To Check
Before moving on, make sure you can:
- separate SQL structure from bound values;
- use allow-lists for dynamic SQL identifiers;
- recognise injection risk outside form submissions;
- apply least privilege to database credentials;
- explain why prepared statements do not replace record-level authorisation.
Practice
Practice: Build A Safe Product Query
Create a prepared-query plan for product search with an allowed sort column.
Requirements
- Bind the search term as a parameter.
- Allow only
name,price_cents, orcreated_atas sort columns. - Reject an unsupported sort value.
Show solution
The sort column is selected from an allow-list and the search value remains a parameter.
<?php
declare(strict_types=1);
function productSearchQuery(string $term, string $sort): array
{
$allowedSorts = ['name', 'price_cents', 'created_at'];
if (!in_array($sort, $allowedSorts, true)) {
throw new InvalidArgumentException('Unsupported sort column.');
}
return [
'sql' => 'SELECT id, name FROM products WHERE name LIKE :term ORDER BY ' . $sort,
'params' => ['term' => '%' . $term . '%'],
];
}
$query = productSearchQuery('lamp', 'price_cents');
echo $query['sql'] . PHP_EOL;
echo $query['params']['term'] . PHP_EOL;
// Prints:
// SELECT id, name FROM products WHERE name LIKE :term ORDER BY price_cents
// %lamp%
Only known SQL identifiers are concatenated. User-controlled values stay in the parameter array.