Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Your LIKE Search Has a Wildcard Injection Bug

C
Carlo Fontanos
· 2 min read

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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading