Skip to content

Cybersecurity

How to prevent SQL injection: parameterised queries and what else to check

SQL injection is prevented by never building queries from strings. Parameterised queries in Go, Node and Python, safe handling of dynamic identifiers, least-privilege roles and testing.

By · Published · 3 min read

Short answer: use parameterised queries (also called prepared statements) for every value that comes from outside your code, and never build SQL by joining strings. The database then receives the query text and the values separately, so input can never change the structure of the statement. Back that with a database role that can do only what the app needs.

What is SQL injection?

It happens when user input becomes part of the SQL text. Suppose a login does this:

// Vulnerable
const sql = "SELECT * FROM users WHERE email = '" + email + "'";

If email is ' OR '1'='1, the query becomes ... WHERE email = '' OR '1'='1' and returns every user. With stacked statements or functions like COPY, an attacker can read other tables, change data or in some setups run commands on the server.

How do parameterised queries fix it?

The placeholder tells the database that this slot is a value, not code. The input is sent separately and is never parsed as SQL.

Go

row := db.QueryRowContext(ctx,
    "SELECT id, name FROM users WHERE email = $1", email)

Node.js with node-postgres

const { rows } = await pool.query(
  "SELECT id, name FROM users WHERE email = $1",
  [email]
);

Python with psycopg

cur.execute("SELECT id, name FROM users WHERE email = %s", (email,))

In Python, do not use an f-string or % formatting on the string itself. The comma and tuple are what make it safe.

What about ORMs?

ORMs parameterise by default, which is a good reason to use one. They become unsafe when you drop into raw SQL and concatenate. Check every raw(), execute(), $queryRawUnsafe and sequelize.query() call. Most ORMs have a safe raw form with bound parameters.

What can't be parameterised?

Placeholders work for values, not for identifiers like table names, column names or ORDER BY direction. A sort feature that takes the column from the URL is a common injection point. Use an allowlist.

const sortable = { name: "name", created: "created_at" };
const col = sortable[req.query.sort] ?? "created_at";
const dir = req.query.dir === "asc" ? "ASC" : "DESC";
const sql = `SELECT * FROM orders ORDER BY ${col} ${dir} LIMIT $1`;

The values placed into the string come from your own map, not from the client.

For IN lists, generate placeholders for the count of items, or in PostgreSQL pass an array: WHERE id = ANY($1). For LIKE, bind the pattern as a value and escape % and _ if you do not want users to inject wildcards.

What else reduces the damage?

  • Least privilege. The application's database role should not own tables, drop them, read other schemas or use COPY ... PROGRAM. A role limited to SELECT, INSERT, UPDATE, DELETE on specific tables turns many injections into small incidents.
  • Input validation as a second layer: types, lengths, formats. It does not replace parameters.
  • Row-level security limits what an injected query can reach across tenants. See PostgreSQL row-level security.
  • Don't show database errors to users. Log them, return a generic message.
  • No stored procedures that build dynamic SQL from strings. The same bug lives inside the database.
  • A web application firewall can block obvious payloads. It is a net, not a fix.

How do you test for it?

  • Search the codebase for string concatenation near query, execute and raw.
  • Use a static analysis tool: gosec for Go, Semgrep or CodeQL for most languages.
  • Try inputs such as a single quote, ' OR 1=1 -- and a very long string on every parameter, in a test environment. A 500 error on a quote is a strong hint.
  • Run an authenticated scan with a tool like OWASP ZAP against staging.
  • Add a unit test that sends a quote character through each repository function.

AI assistants sometimes write string-built queries, particularly in quick scripts. Review those, as covered in is vibe coding safe.

References

Author

Raktim Ranjit is a software engineer and the founder of NodeDR Infotech. He builds and maintains the software described here.

Have something in mind?

Let’s build something useful.

Tell me about the idea, product, or workflow you’re working through.

Tap to say hello