FlawOpen

flawopen.com/SQL Injection/Numeric IDs

Why is string formatting dangerous even for numeric IDs?

Reference page
Short answer

The risk was never about the value's real-world meaning — it's about the fact that the query is built by gluing text together at all. "This is always a number" is an assumption about the data, not a guarantee enforced by the code.

Where the assumption breaks

HTTP parameters arrive as strings, always

A URL query parameter or form field is text on the wire, regardless of what the database column behind it expects. Nothing stops an attacker from sending id=1 OR 1=1 instead of a number — the web framework doesn't reject it before your code sees it unless you explicitly validate the type.

A future refactor can quietly change the source of the value

Code that's safe today because the ID genuinely always comes from a trusted internal source can become unsafe the moment a new caller passes a user-supplied value into the same function — the vulnerability was latent the whole time, waiting for that change.

Type coercion can hide the actual value

In dynamically typed languages especially, a value that looks numeric when logged or debugged can still be a string containing extra characters — the type you expect and the type you actually have aren't guaranteed to match without explicit validation.

Parameterize the query, even after type validation

Validate an ID against the application’s expected format, then pass it through the database driver’s parameter API. Validation protects the input contract; parameterization keeps the input from being interpreted as SQL syntax. Driver behavior on the wire can vary, so do not assume every driver transmits values the same way. PostgreSQL parameters cannot represent table or column identifiers; select dynamic query structure from a fixed application allowlist.

Bind the numeric value; validate its meaning

Validate an ID as a positive safe integer when the application requires that type, but still bind it as a query value. Validation protects the application’s input contract; parameterization keeps input out of SQL syntax. PostgreSQL parameters cannot stand in for table or column identifiers, so choose any dynamic query structure from a fixed application allowlist.

UNSAFE — interpolate the ID into SQL
const rawId = new URL(request.url).searchParams.get("id");
const result = await pool.query(
  `SELECT id FROM users WHERE id = ${rawId}`
);
SAFE — validate the ID and bind it as a value
const rawId = new URL(request.url).searchParams.get("id");
if (rawId === null || !/^[0-9]+$/.test(rawId)) {
  return new Response("Invalid id", { status: 400 });
}
const id = Number(rawId);
if (!Number.isSafeInteger(id) || id < 1) {
  return new Response("Invalid id", { status: 400 });
}
const result = await pool.query(
  "SELECT id FROM users WHERE id = $1",
  Array.of(id)
);

References

See also SQL Injection (CWE-89): How It Works and How to Prevent It in Every Language — flawopen.com What Is a Parameterized Query? Is Prisma $queryRaw Safe from SQL Injection? Is Rails where() String Interpolation Safe? Is Sequelize query() Safe from SQL Injection? Is SQLAlchemy text() Safe from SQL Injection? Does sqlx's Compile-Time Checking Prevent SQL Injection?