Your LIKE Search Has a Wildcard Injection Bug
This code is "safe" - prepared statement, bound parameter, textbook:
$stmt = $pdo->prepare('SELECT * FROM customers WHERE name LIKE ?');
$stmt->execute(['%' . $_GET['q'] . '%']);
Now search for %. The pattern becomes %%% - matches every row. Search a%b%c% on a large table and the scan goes pathological. Underscore is sneakier: _ matches any single character, so a search for "j_n" quietly returns jan, jon, jun - user-visible wrong results that nobody files as a security bug because it looks like fuzzy matching.
Prepared statements protect the SQL structure. % and _ aren't SQL - they're LIKE's own metacharacters, living inside the bound string, perfectly legal. Different layer, different escaping:
function likeContains(string $term): string
{
return '%' . addcslashes($term, '%_\\') . '%';
}
$stmt = $pdo->prepare("SELECT * FROM customers WHERE name LIKE ? ESCAPE '\\\\'");
$stmt->execute([likeContains($_GET['q'])]);
addcslashes backslash-escapes the two wildcards and the backslash itself (forgetting that third character is the classic half-fix - a user typing \ could then un-escape your escaping). The ESCAPE clause pins the escape character explicitly rather than trusting the driver's default - MySQL defaults to backslash, other databases don't, and NO_BACKSLASH_ESCAPES mode changes the rules mid-game. Declare it and the query means the same thing everywhere.
The rest of a sane search endpoint
$q = trim($_GET['q'] ?? '');
$q = mb_substr($q, 0, 60); // length cap: pathological patterns die here
if (mb_strlen($q) < 2) {
return []; // don't scan the table for "a"
}
$stmt = $pdo->prepare(
"SELECT id, name FROM customers WHERE name LIKE ? ESCAPE '\\\\' LIMIT 25"
);
$stmt->execute([likeContains($q)]);
Length caps and LIMIT are the performance half of the fix - leading-wildcard LIKE can't use ordinary indexes, so every search is a scan whose cost you're bounding. (Past a few hundred thousand rows, that's your cue for FULLTEXT indexes or a search engine; LIKE is the prototype that refuses to die, so at least make it a safe prototype. The frontend half of a good search box - debounce and stale-request cancellation - is its own tutorial.)
The pattern to internalize: every mini-language needs its own escaping. SQL gets parameters, LIKE gets wildcard escaping, regex gets preg_quote, HTML gets htmlspecialchars, shell gets escapeshellarg. Injection bugs are almost always one layer's metacharacters riding through another layer's escaping - name the layers and the bugs become visible.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.