Skip to content
Lesson 2 of 8

Injection and SQL Injection

8 min read

What an Injection Attack Is

Injection is one of the oldest and most dangerous classes of vulnerability, and it features prominently in the OWASP Top 10 (category A03:2021 — Injection). It occurs when an application takes untrusted, user-supplied data and inserts it into a command or query that is later executed. The interpreter (the database, the operating system shell, the template engine) cannot distinguish between the legitimate code written by the developer and the malicious data sent by the attacker.

The root of the problem is the mixing of code and data. An interpreter receives a single string and must decide which part is instruction and which part is value. When the developer builds that string by pasting user input into the middle of the instruction, they hand the attacker the power to rewrite the grammar of the query. The input stops being a simple value and becomes part of the executable structure. Everything else —bypassing the login, reading other tables, deleting records— is a consequence of that initial confusion between what is code and what is data.

SQL injection is the best-known variant, but the same root cause appears across many families: OS command injection, LDAP injection, server-side template injection (SSTI), and even NoSQL injection in document-oriented databases. They all share the same underlying defense: strictly separate code from data, so that user input can never change the structure of what gets executed.

How SQL Injection Works

SQL injection (CWE-89) is the best-known variant. Imagine a login form where the application builds a query by concatenating the username and password the user submitted. If those values are inserted as-is into the SQL string, an attacker can introduce special characters (such as a quote) to prematurely close a text literal and then append their own SQL conditions.

Conceptually, the attacker manipulates the query so that the comparison clause is always true, thereby bypassing authentication without knowing a valid password. More advanced variants allow the use of operators like UNION to combine the results of the original query with data from other tables, thus extracting information that was never meant to be accessible. We do not include full payloads here because the goal is to understand the mechanism, not to enable abuse.

Types of SQL Injection

Not every SQL injection returns data directly on the page. It is worth knowing the main families. In-band injection is the most direct: the results appear in the same response, for example through a detailed database error or a UNION query. It is the easiest to detect and exploit.

Blind injection occurs when the application shows neither the data nor the errors, but its behavior changes depending on whether an injected condition is true or false. The attacker infers information by observing differences in the responses. The time-based variant introduces artificial delays in the database to infer data bit by bit based on how long it takes to respond. Finally, out-of-band injection exfiltrates data through a different channel, such as an outbound DNS or HTTP request, and is useful when the direct response channel is closed.

The Real Impact

The impact of a SQL injection can be catastrophic. In the worst case, an attacker can read the entire database: credentials, personal data, card information, business secrets. Many of the largest data breaches in history started with a simple SQL injection in a forgotten parameter: a search field, a sort filter, or an identifier in the URL that no one thought to parameterize.

Beyond reading, depending on the permissions of the database account, an attacker might modify or delete records, alter prices, create administrator accounts, or even, in some configurations, execute commands on the server's operating system. That is why injection is considered high severity: it combines ease of exploitation with a potentially total impact on the confidentiality, integrity, and availability of the data.

Vulnerable Code and Its Fix

The best way to internalize the defense is to see the insecure pattern next to its secure version. In every example, the problem is the same: user input is pasted inside the SQL string. The solution is also the same: send the query and the data separately, using placeholders.

Node.js

In the vulnerable pattern, the user's variable is concatenated directly into the query text. The engine receives a single string and cannot tell where the instruction ends and the data begins:

// VULNERABLE: string concatenation
import { pool } from "./db.js";

async function findUser(email) {
  // The value of `email` becomes part of the SQL code.
  const sql = "SELECT id, name FROM users WHERE email = '" + email + "'";
  const { rows } = await pool.query(sql);
  return rows;
}

The secure version uses a parameterized query with placeholders ($1 in pg, ? in mysql2). The SQL structure stays fixed and the value travels separately, always treated as pure data:

// SECURE: parameterized query
import { pool } from "./db.js";

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

Python

The same mistake appears when the query is built with an f-string or the % operator. Even if the code looks clean, the user input ends up inside the instruction:

# VULNERABLE: f-string inside the query
def find_user(cursor, email):
    # `email` is interpolated into the SQL text before execution.
    cursor.execute(f"SELECT id, name FROM users WHERE email = '{email}'")
    return cursor.fetchall()

The secure version passes the values as the second argument to execute. The database driver applies the placeholder (%s in most drivers, ? in sqlite3) and binds the data safely:

# SECURE: bound parameters
def find_user(cursor, email):
    cursor.execute("SELECT id, name FROM users WHERE email = %s", (email,))
    return cursor.fetchall()

An ORM such as SQLAlchemy or Django's ORM parameterizes by default: when you write User.query.filter_by(email=email) or User.objects.filter(email=email), the library generates bound queries under the hood. You only lose that protection if you drop back to raw SQL with manual interpolation, so avoid building SQL strings by hand even inside an ORM.

How to Prevent It

The primary and definitive defense against SQL injection is parameterized queries (also called prepared statements). With this technique, the structure of the SQL query is defined separately from the data. The user's values are sent as bound parameters, and the database engine always treats them as pure data, never as executable code. This eliminates the root cause: there is no longer any way for user input to change the structure of the query.

As additional layers of defense in depth:

  • Least privilege on the database account. The application should not connect as a superuser. Grant only the permissions it needs (for example, no DROP and no access to system tables) so that even if an injection occurs the damage is limited.
  • Well-built ORMs and data access layers. Adopt tools that parameterize by default and reserve raw SQL for justified cases, always with parameters.
  • Allow-list validation. Some fragments cannot be parameterized, such as a column name in a dynamic ORDER BY. In those cases, validate the input against a closed set of permitted values instead of trying to clean it.
  • Generic errors in production. Disable detailed database error messages facing the user; they reveal the internal structure and make blind injection easier. Log the details on the server side only.
  • Do not rely on manual escaping. Escaping by hand is fragile: it is easy to miss a case, and the rules vary across engines and encodings. Parameterization is robust because the engine never reinterprets the data as code.

To detect these flaws before they reach production, rely on legitimate, authorized testing tools: Burp Suite and OWASP ZAP as analysis proxies, SAST (static code analysis) and DAST (dynamic analysis of the running application) scanners, and sqlmap as a verification tool in test environments against your own systems or with explicit permission. These tools confirm the defense; they do not replace writing parameterized queries from the start.

Prevention Checklist

  • Always use parameterized queries / prepared statements; never concatenate user input into SQL.
  • Prefer an ORM that parameterizes by default and avoid raw SQL with manual interpolation.
  • Apply least privilege to the application's database account.
  • Use allow-lists to validate fragments that cannot be parameterized (column names, sort direction).
  • Disable detailed error messages in production and log them on the server only.
  • Do not depend on manual character escaping as your only defense.
  • Integrate SAST/DAST scanners into the pipeline and test with Burp Suite, OWASP ZAP, or sqlmap only against authorized systems.