Search

What Is SQL Injection and How to Prevent It

The short answer

Quick answer: SQL injection is a vulnerability that occurs when an application builds a database query by pasting user input directly into the SQL text. Because the database cannot tell which part was written by the developer and which part came from the user, specially crafted input can change what the query does: reading other people's data, bypassing a login, or modifying and deleting records. The fix is simple and reliable: use parameterised queries (prepared statements), which send the SQL and the data to the database separately, so input is always treated as a value and never as code.

How it happens

Here is code that looks up a user by name, written the dangerous way:

# VULNERABLE: never build queries like this
query = "SELECT * FROM users WHERE name = '" + name + "'"
cursor.execute(query)

If name is alice, the database receives:

SELECT * FROM users WHERE name = 'alice'

That works as intended. The trouble is that the input is not restricted to names. A single quote in the input ends the string the developer started, and anything after it is read as SQL.

The classic illustration is input containing a quote followed by a condition that is always true. The WHERE clause then matches every row, and a query meant to return one user returns all of them. In a login form built this way, the same trick can make the password check always succeed.

The root cause is one idea: code and data were mixed in the same string. The database receives one piece of text and parses all of it as SQL. It has no way of knowing where the developer's intent stopped and the user's input began.

This is the same family of flaw as cross-site scripting, where untrusted input is interpreted as HTML or JavaScript. The general name is injection.

What an attacker can do

Depending on the query and the database permissions, a successful injection can allow:

  • Reading data the user should never see: other customers' records, password hashes, payment details.
  • Bypassing authentication.
  • Changing or deleting data.
  • Running administrative operations on the database.
  • In badly configured systems, reaching the underlying server.

Even when the application shows no query results and no error messages, attackers can extract data by asking yes-or-no questions and observing differences in the page or its response time. This is called blind injection. So hiding error messages is not a defence.

SQL injection has been understood since the late 1990s and has still been behind many large data breaches. It persists because string concatenation is the most obvious way to build a query. The Wikipedia article lists notable incidents.

The fix: parameterised queries

Write the query with placeholders and pass the values separately:

# SAFE
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
// SAFE (Node.js with pg)
await db.query("SELECT * FROM users WHERE name = $1", [name]);
// SAFE (Java)
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, name);

Here is why it works. The database receives the SQL template first and parses it into a plan (see how query planners work). The structure of the query is now fixed. The values arrive afterwards and are slotted into the plan as data. Whatever characters they contain, quotes, semicolons, keywords, they cannot alter the structure, because parsing is already finished.

A name containing a quote is then simply a strange name that matches no user.

This is the primary defence recommended by the OWASP SQL Injection Prevention Cheat Sheet. Use it for every query that includes any value from outside the code, without exception.

What about ORMs?

Object-relational mappers and query builders (Django ORM, SQLAlchemy, Hibernate, Prisma, Active Record) generate parameterised queries for you. Using them normally is safe.

They all provide a way to run raw SQL, though, and that brings the risk straight back:

# VULNERABLE, even inside an ORM
User.objects.raw("SELECT * FROM users WHERE name = '%s'" % name)

# SAFE
User.objects.raw("SELECT * FROM users WHERE name = %s", [name])

Check any raw query, and any place where a string is formatted into SQL.

What placeholders cannot do

Parameters can stand in for values. They cannot stand in for identifiers or keywords: table names, column names, or ASC/DESC.

If a user chooses which column to sort by, you cannot pass it as a parameter. The safe approach is an allow-list: map the user's choice to a fixed set of known values in code.

SORT_COLUMNS = {"name": "name", "date": "created_at", "price": "price"}
column = SORT_COLUMNS.get(request_sort, "created_at")   # default if unrecognised
query = f"SELECT * FROM products ORDER BY {column}"

The user's text never reaches the query. Only one of your own constants does.

For IN (...) lists, generate the right number of placeholders and pass the values as parameters.

Defence in depth

Parameterised queries are the fix. These limit the damage if something is missed.

LayerWhat it does
Least privilegeThe application's database account should have only the permissions it needs: no rights to drop tables or read unrelated schemas
Input validationIf a field should be a number, an email address or one of five options, check that. It also improves data quality
Stored proceduresSafe if they use parameters internally; unsafe if they build SQL strings themselves
Generic error messagesDo not show database errors to users; log them privately
Web application firewallBlocks many known attack patterns; a supplement, never a substitute
Hashed passwordsIf user data does leak, properly hashed passwords are far less useful. See how passwords should be stored
MonitoringAlert on unusual query patterns and error spikes

What does not work

  • Escaping by hand. Rules differ by database and character encoding, and it is easy to miss a case. Use it only as a last resort.
  • Blocking "bad" words such as SELECT or --. Legitimate input contains them, and there are endless ways to slip past a filter. Pattern matching is the wrong tool; see how regular expressions work.
  • Client-side validation. Anyone can bypass the browser and send requests directly.
  • Assuming a field is safe because it is a hidden input, a cookie, a header, or data already in your database. Any value that originated outside your code can carry an injection, including one stored earlier and used in a query later (second-order injection).

The same flaw elsewhere

SQL is just the best-known target. The pattern, building a command by concatenating untrusted input, also produces:

  • NoSQL injection, in queries built from user-supplied objects or strings.
  • Operating system command injection.
  • LDAP and XPath injection.
  • Prompt injection in applications built on language models, where instructions hidden in data are followed as commands. See how AI agents use tools.

The principle is always the same: keep instructions and data in separate channels.

Finding it in your code

  • Search for SQL built with string concatenation or formatting.
  • Use static analysis tools that flag these patterns.
  • Review every raw query in code review.
  • Test with security scanners as part of your pipeline.

Frequently asked questions

What is SQL injection in simple terms?

A flaw where text entered by a user is treated as part of a database command, letting them change what the command does.

How do I prevent SQL injection?

Use parameterised queries for every query that includes external input. Add least-privilege database accounts and input validation as extra layers.

Do ORMs prevent SQL injection?

Yes, when used through their normal query interfaces. Raw SQL written through an ORM is as vulnerable as any other string-built query.

Is input sanitisation enough?

No. Filtering and escaping are error-prone. Parameterisation removes the problem at its source by never mixing data into the query text.

Conclusion

SQL injection comes from one mistake: letting user input become part of the query's code. Parameterised queries make that impossible, and they are available in every language and database library. Use them everywhere, allow-list anything that cannot be parameterised, and give the application's database account no more power than it needs.

Related articles

Sources and further reading

Usama Muneer

Usama Muneer

Coder, Blogger, Tech Speaker & Web Technologies Enthusiast. Passionate about working on open-source Programming languages & Tools while utilizing my Product Development skills.

Your experience on this site will be improved by allowing cookies Cookie Policy