Safeguard
Application Security•

How do you prevent SQL injection in TypeScript and Node apps?

Use parameterized queries for every value, map dynamic identifiers to allow-lists, run the app with a least-privilege database user, and keep types honest at runtime.

Safeguard Security Team
4 min read

Prevent SQL injection in TypeScript by sending values as query parameters, never by building SQL with string concatenation or template literals. Map any dynamic table, column or sort direction to an allow-list in code. Run the application with a least-privilege database account, and validate input at runtime because types disappear after compile.

What does the safe version look like?

OWASP lists parameterized queries as the first defense. The node-postgres documentation says the same thing in its own terms: avoid concatenating parameters into query text, because that can lead to SQL injection. Pass the unaltered text and the values separately, and the server substitutes parameters safely.

const text = 'SELECT id, email FROM users WHERE email = $1 AND active = $2';
const values = [email, true];
const res = await client.query(text, values);

The unsafe version builds the string with a template literal that embeds the email variable. It compiles, passes type checks, and fails the first time someone submits a quote character plus SQL. TypeScript cannot see that difference, because both are strings.

Why do types not save you?

TypeScript types are erased at compile time. A request body typed as a string is whatever JSON the client sent. If a number field arrives as an object or an array, the driver may convert it in surprising ways. node-postgres converts arrays to Postgres arrays, objects with a toPostgres method through that method, and other objects through JSON stringification. That is fine inside a parameter, but a reason to validate shape before the query.

Validate with a schema library at the boundary, then pass parsed values onward. Validation is a second defense. OWASP says input validation is recommended in all cases as secondary, even with bind variables.

What about table names and ORDER BY?

Parameters cannot stand in for identifiers. The node-postgres docs say PostgreSQL does not support parameters for identifiers, and point to a formatting package for dynamic names. OWASP's advice is stricter: values for table names, column names and sort order should come from code. Map the incoming value to a legal one:

const sortColumns: Record<string, string> = { name: 'name', created: 'created_at' };
const column = sortColumns[req.query.sort as string];
if (!column) throw new Error('bad sort');
const direction = req.query.desc === 'true' ? 'DESC' : 'ASC';
const sql = `SELECT id, name FROM projects ORDER BY ${column} ${direction}`;

Every piece appended to the string comes from a constant in your code, not from the request. Use a plain lookup that cannot match inherited keys such as constructor, for example a Map or Object.hasOwn, since a record lookup on request input can return a prototype property.

Do ORMs and query builders solve it?

They help when you use their parameter interfaces. OWASP names ORMs as one way to build queries safely. The risk returns with raw query escape hatches that accept a string. Treat each raw call like hand-written SQL and review it.

What else limits the damage?

OWASP recommends least privilege as a defense in depth, so the account your app uses should not own the schema or read tables it does not need. It also warns that stored procedures are not automatically safe, and that escaping all input is strongly discouraged as fragile.

What to do

  1. Search the codebase for query calls built with template literals or concatenation, and convert them to parameters.
  2. Validate request shape with a runtime schema before data reaches a query.
  3. Replace dynamic identifiers with lookups to constants.
  4. Audit every raw query escape hatch in your ORM or query builder.
  5. Give the application a database role with only the privileges it needs.
  6. Add a lint rule or review checklist item that bans string-built SQL.
  7. Add a test that submits a quote character in every text parameter and expects normal handling.
  8. Keep database driver and ORM packages updated and scanned.

Safeguard's dependency scanning can flag vulnerable driver and ORM versions in pull requests. It does not replace the code-level changes above.

Quick check

Is escaping quotes ever enough?

OWASP lists escaping all input as a strongly discouraged option because it is fragile and database specific. Use parameters instead.

Can I parameterize a column name?

Not in PostgreSQL. Map the user choice to an allow-list of known column names in code and append only that constant.

Sources

Last reviewed: 18 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.