Mesrai
Back to blog
// essaySecurity

The SQL Injection Mesrai Caught in a Search Endpoint (Real Diff)

Real PR. Real diff. One missing parameterization in a search endpoint. Mesrai posted the review comment in three minutes — full walkthrough with the buggy code, the attack vector and the fix.

Mesrai TeamJune 2, 20269 min read
The SQL Injection Mesrai Caught in a Search Endpoint (Real Diff)

This is a real review comment Mesrai posted on a real PR last week. The PR was small — 31 lines, single file, looked like a routine search-endpoint addition. Three minutes after it opened, Mesrai dropped an inline comment flagging a SQL injection vector. The author had not seen it. Neither had the human reviewer who was about to approve it.

This post walks through what the bug looked like in the diff, why it would have shipped, what the exploit path was, and what Mesrai's review comment said. Anonymized but otherwise unchanged.

The setup

A B2B SaaS team is adding a product search endpoint to their internal admin tool. Stack: TypeScript on Node, Postgres via the `pg` driver, Express router. The endpoint takes a query parameter `q` and returns matching products. Standard CRUD work.

The author opened a 31-line PR adding a single file: `api/search.ts`. Two senior engineers were on review duty that day. Mesrai was installed on the repo with the security rule pack enabled, severity set to medium.

Here is the original diff, abbreviated:

tsapi/search.ts
// api/search.ts — original PR
import { Router } from "express";
import { db } from "../db";

export const searchRouter = Router();

searchRouter.get("/products", async (req, res) => {
  const q = req.query.q as string;
  if (!q) {
    return res.status(400).json({ error: "q required" });
  }

  const sql = `SELECT * FROM products WHERE name LIKE '%${q}%'`;
  const rows = await db.query(sql);
  return res.json({ results: rows });
});

The bug

Line 19 builds the SQL using a JavaScript template literal that interpolates `q` directly into the query string. The author validated that `q` was present, but not that its content was safe. That is the entire bug — one missing layer of separation between user input and the SQL string.

To anyone who has lived this before, the pattern is obvious. To someone writing the endpoint at 4pm on a Friday, it is one line that looks like dozens of similar lines in the codebase. The eye slides past it.

Why it would have shipped

Three reasons. First, the test the author wrote passed cleanly: `q=widget` returns the widget. Second, the human reviewer was working through a 4-PR queue and the rest of the code was fine — clear typing, good error handling, a sensible 400 on missing input. Third, the codebase had no lint rule for raw SQL — most queries went through an ORM, this one was a one-off.

This is the boring kind of bug AI review actually catches. Not the clever exploit you would build a CTF challenge around. Just the gap between "I validated the input is present" and "I validated the input is safe."

The attack vector

The exploit is textbook. An attacker sends:

bashexploit.sh
curl 'https://api.example.com/products?q=%25%27%20OR%20%271%27%3D%271'
# decoded: q=%' OR '1'='1

Which produces this SQL on the server:

sqlexecuted.sql
SELECT * FROM products WHERE name LIKE '%%' OR '1'='1%'

Result: every row in the `products` table is returned, regardless of name. From there the attacker can pivot — `UNION SELECT` to read other tables, error-based fingerprinting of the schema, or in the worst case, second-order injection if any returned data ever hits a `WHERE` clause downstream.

What Mesrai's review comment said

Three minutes after the PR opened, Mesrai posted an inline comment on line 19. Reproduced here in full:

textreview-comment.txt
mesraipilot · Bot · reviewed 3 min ago

[mesrai] [code-review] [Security] [severity: critical] [critical]

Raw user input concatenated into SQL. `q` arrives from `req.query.q`
with no validation or escaping — a textbook injection vector. Use
parameterized queries; never build SQL with template literals over
untrusted input.

Suggested fix:

  const sql = "SELECT * FROM products WHERE name LIKE $1";
  const rows = await db.query(sql, [`%${q}%`]);

The `pg` driver supports $1, $2... parameter placeholders. Postgres
escapes the values for you — the SQL string and the data path stay
separate. This is the only safe pattern for raw queries.

If you have many queries like this, consider adding a lint rule:
  - no-template-literal-sql (block `\${...}` inside SQL strings)
  - or migrate this endpoint to the ORM you already use elsewhere.

Two things to note about the comment. It identifies the exact pattern, not just the line. And it suggests a fix that uses the library the team already has (`pg`), not a new dependency. Mesrai's defaults stay inside whatever conventions the codebase already uses.

The fix

The author pushed the corrected version 14 minutes later. The diff was three lines changed:

tsapi/search.ts (fixed)
// api/search.ts — corrected
import { Router } from "express";
import { db } from "../db";

export const searchRouter = Router();

searchRouter.get("/products", async (req, res) => {
  const q = req.query.q as string;
  if (!q) {
    return res.status(400).json({ error: "q required" });
  }

  const sql = "SELECT * FROM products WHERE name LIKE $1";
  const rows = await db.query(sql, [`%${q}%`]);
  return res.json({ results: rows });
});

Mesrai re-ran the review on the updated diff. The injection finding was gone. One small win: Mesrai also noted the team should add a `% _ \` escape on the `LIKE` value if attackers can use wildcards to enumerate rows. That is a separate finding, lower severity, which the team filed for follow-up.

The human reviewer's response

The senior reviewer who would have approved the PR sent a message in the team Slack the next morning. Anonymized:

“Honestly thank god for that bot. I had this PR open in another tab and was going to LGTM after lunch. The endpoint looked fine. I would have read past that line because everything around it was clean. That's exactly the kind of bug review fatigue hides.”

This is the additive principle at work. The AI did not replace the senior reviewer. It freed them from the throughput layer — "is this PR even safe to merge?" — so they could spend their attention on the judgement layer: "does this endpoint belong here? does the response shape make sense? are we leaking too many fields?"

What this bug is not

It is not a clever exploit. It is not a novel attack pattern. It is not something only AI could catch. A senior engineer who was sharp and not in a hurry would have caught it in any code review. That is the point — "sharp and not in a hurry" is not a sustainable property of human review at scale.

This is the boring 70% of bugs AI review actually helps with. The clever 30% — the architectural mistakes, the misread requirements, the wrong-trade-off calls — still need humans. Mesrai's positioning has always been that AI handles the boring 70%; the senior reviewers focus on what's left.

How to wire this rule on your repo

If you are running Mesrai already, the security rule pack ships enabled by default at medium severity. No configuration needed for this finding. If you want the LIKE-escape note as a warning, add it as a custom rule:

yaml.mesrai.yml
# .mesrai.yml in repo root
severity: medium
packs:
  - security
  - performance
  - architecture

custom_rules:
  - name: like-wildcard-escape
    description: |
      For SQL LIKE clauses on user input, escape % and _ before
      passing to the parameterized query. Otherwise users can
      enumerate rows by including the wildcards in their input.
    severity: warning
    languages: [ts, js, py]

This is the kind of one-line custom rule that pays back instantly on a team where multiple devs are touching raw SQL. Mesrai applies it on every PR going forward.

Takeaway

One concat. One injection. Three minutes to catch. Fourteen minutes to fix. The PR shipped clean. The team did not need a second incident to learn from.

If you have not wired AI review into your PR queue yet — this is the category of bug it earns its keep on. Not the dramatic ones. The boring ones that the reviewer reading their fourth PR of the day misses because the rest of the code looks fine.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.