All checks

SQL injection

Severity: CriticalQuick + Deep· 6 min read

What it is

SQL injection happens when your app builds a database query by gluing user input into the SQL text. The database can't tell where your query ends and the user's input begins, so carefully chosen input changes what the query does.

Think of a form letter: "Please pay ____ to ____." If someone fills in the blank with "nothing to anyone. Also, pay everything to me", and you print it as written, the letter now says something you never meant. Parameterized queries are like using a form whose blanks can only ever hold a value, never new instructions.

Why it matters

A single injectable query can expose far more than the data it was written for. Depending on the database and its permissions, an attacker may be able to read other tables (user accounts, password hashes, payment records), change or delete data, or skip a login check entirely.

It's one of the oldest web vulnerabilities and still common, which is why injection remains in the OWASP Top 10. It's also one of the easiest to rule out completely, because the fix is mechanical.

How it happens

The vulnerable pattern is string building: template literals, concatenation, f-strings or format calls that put request values into SQL.

// GET /api/products?search=...app.get("/api/products", async (req, res) => {  const search = req.query.search;  const rows = await db.query(    `SELECT * FROM products WHERE name LIKE '%${search}%'`  );  res.json(rows);});

ORMs don't protect you automatically. Most offer a raw-SQL escape hatch, such as $queryRawUnsafe, raw(), whereRaw or .extra(), and string-building into those is exactly as dangerous.

How it's abused

At a conceptual level, without ready-made payloads:

  1. Find inputs that reach the database: search boxes, filters, sort options, IDs in URLs, JSON fields.
  2. Send input containing characters that matter in SQL, such as quotes, and watch for errors or changed results. Either suggests the input reaches the query unescaped.
  3. Shape the input so the query returns or does something else, for example extra rows, data from another table, or a condition that is always true.
  4. Automate extraction once a working shape is found.

Error messages make step 2 easier, which is one reason production apps shouldn't return raw database errors. But hiding errors only slows an attacker down; the fix is the query itself.

How to fix it

Use parameterized queries: send the SQL and the values separately, so input is always treated as data. Every database driver supports them.

app.get("/api/products", async (req, res) => {  const search = String(req.query.search ?? "");  const rows = await db.query(    "SELECT * FROM products WHERE name LIKE $1",    [`%${search}%`]  );  res.json(rows);});

Why this works: with a placeholder ($1, %s, ?), the driver sends the value separately from the SQL text. The database parses the query first, then fills in the value, so the value can never become part of the query's structure.

More good practice:

  • Prefer your ORM's query builder over raw SQL. When you need raw SQL, use the parameterized form ($queryRaw with a tagged template in Prisma, text() with bound parameters in SQLAlchemy).
  • Allowlist what can't be a parameter. Table names, column names and sort directions can't be bound. Map user choices to a fixed list instead of inserting them.
  • Use a least-privilege database user so an injection, if one slips through, can't drop tables or read everything.
  • Don't return raw database errors to users.

Common mistakes

  • Parameterizing values but not identifiers. ORDER BY ${req.query.sort} is still injectable. Map allowed sort columns explicitly.
  • Building IN (...) lists by hand. Use your driver's array parameter (= ANY($1) in Postgres) or generate one placeholder per value.
  • Escaping manually "just here". Hand-written escaping drifts between databases and encodings. Use placeholders everywhere.
  • Trusting internal input. Values from your own database, a queue or a webhook can carry user-controlled text, too. Parameterize them like everything else.

Testing your fix

Search your codebase for raw-SQL calls and string building near them: template literals or f-strings containing SELECT, INSERT, UPDATE, DELETE or WHERE. Each should use placeholders. Then run your app's search and filter features with input containing quotes and percent signs, which should simply be searched for, with no errors and no extra results.

How SetRepo detects it

SQL injection runs on every Quick and Deep scan, using SetRepo's own taint rules. Each rule traces request input (query strings, bodies, route params) to a raw SQL call with no parameterization in between. The rules cover JavaScript and TypeScript, Python, Go, Java and Kotlin, PHP, Ruby, Rust and C#. Python code is also checked by Bandit, which includes SQL-construction checks.

Each finding shows the file and line where input reaches the query, with a fix prompt to switch to a parameterized query. Pattern matches like this are usually labeled "verify" and reported no higher than High until confirmed. On Deep scans, uncertain critical and high findings get a second look from the AI review, which reads the surrounding code and confirms or dismisses them.

SetRepo also checks related injection paths: NoSQL injection (for example into MongoDB $where expressions) and Supabase filter strings built from user input.

Checklist

  • Every query that includes user input uses placeholders or a query builder.
  • No template literals, f-strings or concatenation inside SQL text.
  • Raw-SQL ORM methods are only used in their parameterized form.
  • Table names, column names and sort orders come from an allowlist.
  • The app's database user has only the permissions it needs.
  • Production responses never include raw database errors.

FAQ

Isn't escaping quotes enough?

No. Escaping is easy to get wrong across databases, encodings and edge cases. Parameterized queries remove the problem instead of patching around it.

I use an ORM. Am I safe?

Mostly, as long as you use its normal query methods. Raw-SQL helpers are the exception: string-building into them is as vulnerable as raw SQL.

Does input validation fix SQL injection?

It helps reduce bad input, but it isn't a substitute. A field that legitimately allows apostrophes, like a name or a search term, still needs parameterized queries.

Are stored procedures safe?

Only if they don't build SQL from their inputs. A stored procedure that concatenates parameters into dynamic SQL is just as injectable. Call procedures with bound parameters, and parameterize any dynamic SQL inside them.

References

Check your app for this, free.

Start free scan