DEV Community

Cover image for What Is SQL Injection? How User Input Can Become a Database Command
Aditya Sharma
Aditya Sharma

Posted on

What Is SQL Injection? How User Input Can Become a Database Command

Consider a login form. A user enters a username and password. Somewhere behind that form, the application needs to ask the database whether those credentials exist. A straightforward query for that looks something like this:

SELECT * FROM users
WHERE username = 'alice'
AND password = '...';
Enter fullscreen mode Exit fullscreen mode

The application constructs that query, sends it to the database, and if a matching row comes back, the user is authenticated. This works. The problem starts with how the query gets constructed.


When Strings Become SQL

The simplest way to build that query is also the most dangerous: concatenate the user's input directly into the SQL string.

query = "SELECT * FROM users WHERE username = '" + username + "'"
Enter fullscreen mode Exit fullscreen mode

When username is alice, the resulting query is sensible SQL. But the database doesn't see username; it sees the final string. It has no idea which characters came from the developer's query template and which came from the user's input. Once the string is assembled, it's all just SQL.

This is where the boundary breaks down.

If a user provides input that contains characters meaningful in SQL syntax, those characters don't stay inside the "data" portion of the query. They become part of the SQL itself. The application intended for the value to be a data literal. The database receives something with a different structure.

This is SQL injection: user-controlled input influences the SQL syntax rather than remaining data within it.

The parallel to XSS is direct. In XSS, untrusted data reaches the browser in a context where it becomes executable markup. In SQL injection, untrusted data reaches the database in a context where it becomes part of a command. The failure mode is the same: a boundary between data and executable instructions has been lost.


The Real Fix: Parameterized Queries

The solution isn't to scan input for suspicious characters and filter them out. The solution is to never combine the SQL structure and the user-supplied values into a single string in the first place.

Parameterized queries do this by separating the two:

query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))
Enter fullscreen mode Exit fullscreen mode

The query template defines the SQL structure. The value is supplied separately. The database driver handles them as distinct things: the structure tells the database what kind of query this is, and the value is data to be used within that structure.

This isn't the database "detecting suspicious input." It's a different communication model entirely. Because the structure and the values are never combined into one string, there's no opportunity for user input to alter the query's shape. The username is always treated as a value to match against a column, regardless of what characters it contains.

Placeholder syntax varies between databases and drivers. Some use ?, others use %s or named parameters like :username. The specific syntax is a detail; the principle is the same across all of them.


Prepared Statements

Prepared statements extend this idea further. The application sends the query template to the database first, the database parses and compiles it, and then values are supplied for execution. The SQL structure is fixed before any user data enters the picture.

The practical effect is similar to parameterized queries: values are always data, never syntax. Some database systems also allow reusing a prepared statement across multiple executions with different values, though that's more of a performance consideration than a security one.


ORMs and Query Builders

Modern ORMs and query builders often parameterize values automatically. When you call something like User.where(username: params[:username]) in an ORM, the library typically handles the parameterization for you. The generated SQL keeps structure and values separate.

But using an ORM doesn't automatically make an application immune to SQL injection. Most ORMs also provide ways to drop into raw SQL when needed. If a developer constructs a raw query with concatenated input through an ORM's escape hatch, the vulnerability is still there. The abstraction helps when you use it correctly; it doesn't protect against deliberately bypassing it.


Why Validation Isn't Enough

Input validation is worth doing. Ensuring that a username only contains letters and numbers, for instance, limits what can be submitted and may reduce certain attack surfaces. But validation is not the fundamental defense against SQL injection.

Validation works at the application level by restricting what input is accepted. Parameterized queries work at the query level by ensuring that whatever is accepted can never alter SQL structure. These are different guarantees. An application that validates inputs but constructs queries through concatenation is still vulnerable if the validation has gaps, or if the attacker finds a legitimate input that's still useful for injection purposes.

The safe path is parameterization first, with validation as an additional layer.


The Boundary That Has to Hold

SQL injection is not really about databases being insecure, or about users being malicious, or about any specific characters being dangerous. It's about what happens when an application assembles a command from a template and external input by treating both as the same kind of string.

The database receives one SQL statement. It parses that statement and executes it. It has no visibility into how the statement was assembled or which parts the developer intended as structure versus which parts a user supplied. If those two things were combined into the same string, the database can't separate them.

Parameterized queries don't give the database better judgment. They change the interface so the developer never has to combine them in the first place. The SQL structure and the user-supplied values travel separately, and the database handles them as separate things.

The problem isn't that users are allowed to enter text. The problem is allowing that text to become part of the program's command language. Keeping data as data, and keeping SQL as SQL, is the entire fix.

Top comments (0)