Web development

SQL injection prevention that holds

SQL injection prevention means parameterised queries, identifiers chosen from closed lists and a least-privilege database user. What stays exposed anyway.

Key points

  • The defect is the hand-built string, not the text someone types.
  • Column name, sort direction and table name are not values: choose them from a closed list.
  • The application database user must not read tables it does not use or run DDL.
  • The catalogue holds 32 form blocks and 34 login blocks out of 1,556: those are the surfaces that send data to a database.

SQL injection prevention comes down to parameterised queries and a least-privilege database user. The defect is the hand-built string, not the text someone types: when the value and the command travel separately, the database has no way to read the value as SQL.

The classic case is nothing exotic. A search field, a sortable column, an «active only» filter: each of those three points, written with concatenation, opens a path to reading or deleting tables the form never named. Looking only at the POST body leaves headers, cookies and sort parameters outside the net.

Parameterised queries, not cleaned strings

The driver sends the query text and the values separately, and the database never reads a value as a command. The string ' OR 1=1 -- becomes text to search for, not a condition. The mechanism is the same in every language: $1 in PostgreSQL, a question mark in MySQL, a named placeholder in SQLite. Hand-written escape functions are a second choice: one wrong encoding or charset and the filter falls over.

The places a parameter cannot reach

    Least privilege: the other half of the defence

    A parameterised query protects the values. A database user that can read the whole user table is still a problem when one wrong query exposes an extra column. The application account gets only what it needs: SELECT, INSERT and UPDATE on the right tables, no DDL and no access to the system catalogue. Migrations use a second account, active only during deploy.

    The input that never touches a form

    Headers, cookies, sort parameters, webhooks, imported files and scheduled jobs carry the same risk as a text field. A filter on the POST body alone covers half the surface. The rule does not change: data is not trusted, it is passed as a parameter, and type validation stays an extra check rather than a defence.

    Errors and logs

    A database error does not go back to the client: the syntax message tells an attacker which fragment worked and which did not. Reply 500 with a request identifier and look for the detail in the logs. The standard error shape for the client, with a type and an instance, is described in designing an API.

    The check you can put in CI

    A grep is not a security test, but it catches the most common regression: the query built by concatenation or string interpolation. The command below looks for the suspect shapes in the project files and checks that the application user is not a superuser. Those two lines do not replace a review: they stop the defect from coming back after it has been fixed.

    # query costruite per concatenazione o interpolazione: da rivedere una per una
    rg -n "SELECT .* FROM .*\\+" --glob "*.{js,ts,py,php}" src/
    
    # l'utente dell'applicazione non deve essere superutente
    psql "$DATABASE_URL" -c "select current_user, usesuper from pg_user where usename = current_user"

    The limitation to state

    No catalogue block secures a server: prevention lives in queries and permissions, and a React component touches neither. A web application firewall cuts scanner noise, but it does not replace parameterisation and it does not catch a badly written raw query. The catalogue carries a limit too: the licence is declared by the publisher and needs checking on each block before you publish.

    Three catalogue records

    The login category holds 34 blocks with email and password fields. The form category holds 32, from contact forms to multi-field ones. Table components cover the operator view, where a broken filter shows up fast. Previews are free; the full code sits in the Pro plan.

    On the authentication side middleware alone is not enough: read authentication middleware, session and cookies and the comparison between server actions and APIs.

    Frequently asked questions

    Are parameterised queries enough to stop SQL injection?

    They stop the part about values, which is most cases. Identifiers built at runtime, such as column name and sort direction, stay exposed, and so do over-broad database permissions. You need all three controls.

    Does an ORM remove the risk?

    No. An ORM parameterises the queries it generates, but every raw SQL method takes a string and reopens the problem. Query fragments composed by hand inside the ORM need parameterising too.

    How do I pass a column name in ORDER BY?

    Through a closed list of allowed values, for example a dictionary mapping «name» to the real column. The name arriving from the query string never enters the query: it only selects an entry from the list.

    Does a WAF not solve the problem?

    It cuts automated scanners and log noise, but it does not know your application: a badly built query stays exploitable behind a WAF. Treat it as one extra layer, not the main defence.

    Which permissions should the application database user have?

    Only what it needs: SELECT, INSERT and UPDATE on the tables it uses, no DDL and no access to the system catalogue. Migrations run with a second account, used only by the deploy.