Skip to content
Unlisted Report logoUnlisted ReportSubscribe
Vulnerabilities

SQL Injection Explained, with Safe Code Examples

SQL injection (SQLi) allows attackers to control an application's database by inserting malicious SQL. Learn how it works and how to prevent it with safe.

By · Published · 13 min read

Abstract digital art representing a database with a glowing red vulnerability point, symbolizing a security flaw like SQL injection.

SQL injection, often abbreviated as SQLi, is a code injection technique used to attack data-driven applications. It occurs when an attacker inserts malicious SQL statements into an entry field for execution, allowing them to bypass security measures and access, modify, or delete data in the application's database. This vulnerability has been a persistent threat for over two decades, consistently ranking among the most critical web application security risks.

What is SQL Injection (SQLi)?

To understand SQL injection, you first need to understand how many modern websites work. When you log in, search for a product, or post a comment, your browser sends that information to a web server. The server-side application then often needs to store or retrieve data from a database. To communicate with the database, the application uses a special language called Structured Query Language (SQL).

For example, when you enter your username and password, the application might construct an SQL query like this to find your user profile: `SELECT * FROM users WHERE username = 'your_username' AND password = 'your_password';`

SQL injection happens when an application insecurely constructs these queries by directly pasting user-provided input into the SQL command. An attacker can supply specially crafted input that tricks the application into running unintended SQL commands. It's like telling a robot librarian to fetch a book by "The Great Gatsby" but sneakily adding "; and also bring all books from the restricted section" onto the end of your request. If the robot isn't programmed to distinguish its instructions from the book title, it will blindly follow the entire command string.

How Does an SQL Injection Attack Work?

The classic example of SQL injection involves bypassing a login form. Let's imagine a website has a simple login process that checks a username. The code might create a query by simply combining strings.

A legitimate query for a user named `jane.doe` would look like this: `SELECT id, username, role FROM users WHERE username = 'jane.doe';`

The application code might look like this, where `user_input` comes directly from a form field: `query = "SELECT id, username, role FROM users WHERE username = '" + user_input + "';"`

An attacker doesn't need to know a real username. Instead, they can enter something like this into the username field: `' OR 1=1 --`

When the application inserts this input into its query string, the final SQL command sent to the database becomes: `SELECT id, username, role FROM users WHERE username = '' OR 1=1 --';`

Let's break down what the attacker did: - `'`: The first single quote closes the string for the `username` value. - `OR 1=1`: This is a logical condition that is always true. The query now asks the database to retrieve users where the username is empty *OR* where 1 equals 1. Since 1 always equals 1, the condition is met for every single row in the `users` table. - `--`: This is an SQL comment. It tells the database to ignore everything that comes after it, effectively neutralizing the rest of the original query, including any password checks or the final single quote.

The database executes this malicious query and returns a list of all users. The application, likely only expecting one result, might grab the first user in the list (often the administrator) and log the attacker in with full privileges.

Types of SQL Injection

While the login bypass is a classic, attackers use several SQLi techniques depending on their goal and what the application reveals.

SQLi TypeDescription
In-Band (Classic)The attack and the data extraction occur over the same channel. This includes Union-based attacks, which use the `UNION` operator to combine the results of a malicious query with a legitimate one, and Error-based attacks, which force the database to produce error messages that leak information about its structure.
Inferential (Blind)The attacker sends queries but gets no direct data back. Instead, they infer information by observing the application's response. Boolean-based attacks ask true/false questions (e.g., "Is the first letter of the admin password 'a'?"), while Time-based attacks inject commands that cause a time delay if a condition is true, allowing the attacker to extract data one character at a time.
Out-of-BandThe attacker causes the database server to make a network connection to a server they control, exfiltrating data over that connection (e.g., via DNS or HTTP requests). This is used when the application's responses are too limited for other methods.

The Impact of a Successful SQLi Attack

A single SQL injection flaw can be catastrophic. The consequences range from minor data leaks to a full system compromise, which is why understanding how data breaches happen often involves analyzing entry points like SQLi.

  • Confidentiality Breach: This is the most common outcome. Attackers steal sensitive data, such as customer personal identifiable information (PII), financial records, intellectual property, or user credentials that can be sold on the dark web or used for further attacks.
  • Authentication Bypass: As seen in the example above, attackers can gain access to user or administrator accounts without needing passwords.
  • Integrity Loss: Attackers can add, modify, or delete data in the database. They might change prices in an e-commerce store, alter a news article, or delete entire tables, causing widespread disruption.
  • Denial of Service (DoS): An attacker can execute a query that consumes excessive resources or locks database tables, making the application unavailable for legitimate users. For example, a time-based query with a very long delay can tie up a database connection.
  • Complete Host Takeover: In poorly configured systems, SQL injection can be escalated to a full-blown remote code execution (RCE) vulnerability. Some database systems have functions that can interact with the underlying operating system. If the application's database account has excessive privileges, an attacker could use SQLi to run shell commands on the server, effectively taking it over.

Code Examples: Vulnerable vs. Secure

Preventing SQL injection starts with writing secure code. The core principle is to never trust user input and to ensure that it is never mixed with executable SQL code.

The Wrong Way: String Concatenation

This Python example uses an f-string to build an SQL query, which is highly vulnerable. Any method that simply pastes user input into a query string is dangerous, whether it's using `+` concatenation, f-strings, or `format()`.

import sqlite3
def get_user_vulnerable(username):
    db = sqlite3.connect(":memory:")
    cursor = db.cursor()
    # DANGEROUS: User input is directly inserted into the query string.
    query = f"SELECT * FROM users WHERE username = '{username}'"
    try:
        cursor.execute(query)
        return cursor.fetchone()
    except sqlite3.Error as e:
        print(f"An error occurred: {e}")
        return None

If a user enters `' OR 1=1 --`, the query becomes `SELECT * FROM users WHERE username = '' OR 1=1 --'`, a classic injection attack.

The Right Way: Parameterized Queries

The correct approach is to use parameterized queries, also known as prepared statements. The SQL code and the user data are sent to the database separately. The database engine then combines them safely, ensuring the user input is treated as literal data and not as part of the SQL command.

In this secure Python example, we use a `?` as a placeholder.

import sqlite3
def get_user_safe(username):
    db = sqlite3.connect(":memory:")
    # Create a dummy table for the example
    db.execute("CREATE TABLE users (id INTEGER, username TEXT, role TEXT)")
    db.execute("INSERT INTO users VALUES (1, 'admin', 'admin')")
    db.commit()
    cursor = db.cursor()
    # SAFE: The query uses a placeholder (?) for user input.
    query = "SELECT * FROM users WHERE username = ?"
    # The user input is passed as a separate argument tuple.
    cursor.execute(query, (username,))
    return cursor.fetchone()

If an attacker enters `' OR 1=1 --` as the username, the database looks for a user whose literal username is the string `' OR 1=1 --`. Since no such user exists, the query fails safely, and no injection occurs.

Safe Queries in Other Languages

This principle applies across all languages and database systems. Here's an example using Node.js with the popular `node-postgres` (pg) library, which uses `$1`, `$2`, etc., as placeholders.

const { Client } = require('pg');
async function getUserSafeNode(username) {
  const client = new Client({
    user: 'dbuser',
    host: 'database.server.com',
    database: 'mydb',
    password: 'secretpassword',
    port: 5432,
  });
  await client.connect();
  // SAFE: The query uses a placeholder ($1).
  const query = 'SELECT * FROM users WHERE username = $1';
  const values = [username];
  try {
    const res = await client.query(query, values);
    return res.rows[0];
  } catch (err) {
    console.error(err);
    return null;
  } finally {
    await client.end();
  }
}

How to Prevent SQL Injection

A defense-in-depth strategy is the most effective way to protect against SQLi.

  1. Use Parameterized Queries: This is the single most important and effective defense. Always use prepared statements or parameterized queries provided by your database driver or framework.
  2. Use an ORM: Object-Relational Mapping (ORM) libraries like SQLAlchemy for Python or TypeORM for Node.js abstract away raw SQL. They typically use parameterized queries by default, making your code safer and less prone to human error.
  3. Apply the Principle of Least Privilege: Configure the application's database user with the bare minimum permissions it needs to function. For example, a user account that only needs to read data should not have `UPDATE` or `DELETE` permissions. It certainly shouldn't have permissions to drop tables or run system commands.
  4. Validate and Sanitize All Input: While not a substitute for parameterization, input validation is a crucial secondary defense. Enforce strict data types (e.g., an ID should be an integer), check for expected formats, and use a whitelist of allowed characters where possible.
  5. Keep All Software Updated: Vulnerabilities can exist in the database management system, web frameworks, or third-party libraries. A diligent patch management process is essential for closing security holes as soon as fixes are available.
  6. Use a Web Application Firewall (WAF): A WAF sits in front of your web application and can inspect incoming traffic for malicious patterns, including common SQLi attack strings. It acts as an external shield, though it should not be relied upon as the sole line of defense.

Is SQL Injection Still a Threat Today?

Yes, absolutely. For over 20 years, SQL injection has been a well-understood vulnerability with clear solutions. Yet it consistently appears on the OWASP Top 10 list of critical web application security risks.

The persistence of SQLi is due to several factors. Legacy systems with old, vulnerable code are still in widespread use. Tight deadlines can lead developers to take shortcuts, like using unsafe string concatenation. A lack of security training can mean developers simply don't know the risks or the proper defenses. As applications become more complex, the number of potential input vectors grows, increasing the chances of a mistake. When a new SQLi vulnerability is discovered in a popular product, it is often assigned a high or critical CVSS severity score due to its potential for widespread damage.

Beyond the Code: Fostering a Culture of Security

Fixing and preventing SQL injection isn't just about using the right library or function call. It's about building a development culture where security is a shared responsibility, not an afterthought.

This means providing developers with continuous training on secure coding practices. It means integrating automated security scanning tools (SAST and DAST) into the development pipeline to catch vulnerabilities early. It means conducting regular, independent security audits and penetration tests to uncover flaws that automated tools might miss.

Ultimately, preventing SQL injection requires a fundamental shift from "does it work?" to "does it work, and is it secure?" By embedding security into every stage of the software development lifecycle, organizations can build more resilient applications and finally turn the tide against one of cybersecurity's oldest and most damaging adversaries.

Read next