---
title: "Parameterized Attacks Beat Placeholder Matching"
canonical: https://dxdev.com/blog/2026-09-26_when-your-guard-only-catches-one-shape/
datePublished: 2026-07-27
---
The guard passed every unit test, and then I asked a fresh Claude Code session to write a file that should have tripped it. The Write succeeded. No hook fired.

## What the guard was for

On July 21 at about 3:28 PM, a session ran a leading-wildcard `LIKE '%...'` scan serially over six cache tables in our shared production database. The worst table took 139 seconds and 835K logical reads. The app's own cache DELETEs hit the 30-second ODBC timeout, and every team page errored for about five minutes. A "read-only probe" still takes locks and I/O, and all our clones point at one live database.

So we wrote a PreToolUse hook. It reads the raw JSON payload on stdin and blocks when two things appear together: a prod-DB connection marker (our credential env vars, a DSN string, `pyodbc`, `sqlcmd`, `Invoke-Sqlcmd`) and a `LIKE` followed by a quoted string starting with `%`. It has no dependencies, just `grep -E` on the payload, so it runs the same for everyone from the checked-in repo. The registration matches `Bash|Write|Edit|MultiEdit`.

It started life on one operator's desktop. This week we shipped it from all three repos. One of them, the Node API that talks to the same live database, had no `.claude` directory at all.

## Two probes that walked past it

The unit tests fed the hook literal SQL like `WHERE cacheKey LIKE '%foo'`. That is the shape the regex was written against, so of course it passed.

Live testing meant prompting fresh sessions to write a script with the wildcard first in the pattern, then report verbatim whether the Write was blocked. Two probes:

1. **Node with `mssql`.** The script did `.input('pattern', sql.NVarChar, '%session')` and then `.query('... WHERE cacheKey LIKE @pattern')`. The `LIKE` is followed by `@pattern`, not a quote, so the scan regex never sees it. Worse, the connection marker never matched either. That API reads `DB_SERVER`, `DB_NAME` and so on, and none of my markers listed those. The port would have shipped a hook that could not fire in that repo.
2. **PowerShell with `Invoke-Sqlcmd`.** The script built `$pattern = '%' + $KeySuffix` and put `LIKE $(pattern)` in the query with `-Variable @("pattern='$pattern'")`. The Write went through.

## Chasing sigils

My first move after probe 2 was to fix the regex. I had already handled the bare-literal case, so I widened it to parameterized ones: `LIKE` followed by a placeholder sigil. I wrote that regex into a replay harness that rebuilds the real PreToolUse payload for the Write and pipes it through the hook, and I ran it against the PowerShell payload:

```
conn: MATCH
scan: no match
param_like: no match
wildcard_literal: MATCH
```

`param_like` was `LIKE[[:space:]]*[@:?\$][A-Za-z0-9_]`. It covers `@name`, `:name`, `?` and `$name`. sqlcmd variable substitution is `$(name)`, a dollar sign followed by a paren, so it slid straight past. I had spent a cycle building the second special case for a guard whose whole job was to stop chasing special cases. Every driver spells its placeholder differently. That is the wrong shape for the match, and I would have kept finding new sigils in live tests, one production near-miss at a time.

The last line of that output was the useful one. `wildcard_literal` was `N?['"][[:space:]]*%`, a quote followed by `%`, with no `LIKE` in front. It matched the PowerShell payload, because the leading wildcard exists as a literal (`'%'`) in the script whether or not the SQL text ever contains it. It also matches the Node payload's `'%session'`.

## Matching where the wildcard is born

The match is now shape-independent. The condition is a connection marker plus a string literal that begins with `%`, anywhere in the payload, with no requirement that the literal sit next to `LIKE`. The wildcard has to originate somewhere as a quoted value, even when the SQL only ever sees a placeholder. Matching that origin covers `@pattern`, `$(pattern)`, `:pattern` and whatever the next driver invents.

The connection markers gained the Node/mssql shape, so the guard can fire in the API repo. Then I re-ran both probes in fresh sessions and did not stop at the unit tests. Both were blocked.

## What it still does not catch

The hook's header already admits a known limit. `CHARINDEX` and `PATINDEX` over a whole table wedge prod just as well, but they have heavy legitimate bounded use, and matching them would false-positive constantly. Markdown files are exempt so incident write-ups can quote the pattern. It is a nudge with teeth, and the wildcard-literal match is broader, so it will occasionally fire on a harmless `'%'`, which I accept.

A unit test written by the person who wrote the regex checks the regex against the author's imagination. Both bypasses came from an agent writing code the way agents actually write it, and only a payload replay from a real session caught them.
