Everyone knows about SQL injection. Nobody writes it on purpose anymore. So how does it keep shipping? Because the model wrote a perfectly normal-looking search endpoint, and somewhere in the middle of that endpoint there is a template string that should have been a parameter.
Injection happens when user input gets treated as code instead of data. SQL injection is the famous one, but the family is larger: XSS in the browser, template injection in server-rendered views, command injection in a shell call, NoSQL injection in a Mongo filter. They all share a single root cause — data and instructions sharing the same channel.
What your AI actually built
You asked for a search endpoint. The model gave you one: take a query string, filter the users table, return the matches. Works on the happy path. Looks like every tutorial you have ever read.
What it did not do was bind the parameter. The search term gets concatenated into the SQL directly, because string interpolation reads naturally and the model has seen a thousand examples that look identical. The ORM call right next to it uses prepared statements — but this one raw query slipped through.
The same pattern lives one layer up in the frontend, where a comment renders through dangerouslySetInnerHTML. And one layer down in a job runner that passes a filename into exec without quoting. Different syntax, same bug: data treated as code.
How it gets exploited
The attacker finds a search box on the public site. They type a single quote and press enter.
- 1Probe the inputA stray quote crashes the endpoint with a Postgres error message. The error leaks the query, which tells them exactly what to inject.
- 2Read the schemaA UNION SELECT against information_schema dumps every table and column name. They find users, sessions, and api_keys.
- 3ExfiltrateA second UNION returns email and password_hash pairs. They page through the results 50 rows at a time.
- 4EscalateThe api_keys table includes a Stripe live key. That key works. They are now pulling customer data from an unrelated service.
A single-character payload turned a read-only search box into a full database dump and a pivot into a payments provider. The web server logs show five innocuous GETs.
Vulnerable vs Fixed
// app/api/search/route.ts
import { sql } from '@/lib/db';
export async function GET(req: Request) {
const q = new URL(req.url).searchParams.get('q') ?? '';
const rows = await sql.unsafe(
"SELECT id, name FROM users WHERE name ILIKE '%" + q + "%'",
);
return Response.json(rows);
}// app/api/search/route.ts
import { sql } from '@/lib/db';
export async function GET(req: Request) {
const q = new URL(req.url).searchParams.get('q') ?? '';
const rows = await sql`
SELECT id, name FROM users
WHERE name ILIKE ${'%' + q + '%'}
`;
return Response.json(rows);
}The tagged template hands the parameter to the driver separately from the query text. The database now treats q as a value, never as SQL. Nothing else about the endpoint changes.
A real case
British Airways lost 380,000 card records to an injected script
Magecart attackers injected 22 lines of JavaScript into a third-party library on the BA checkout page — the same class of bug, moved to the client side.
Related reading
References
Find every place user input ends up in a query.
Flowpatrol probes every route on your app and confirms each injection finding with a real payload. Five minutes. One URL.
Try it free