Security

SQL Injection in 2026: Still a Problem, Here's How to Stop It

SQL injection remains a top vulnerability. Learn how SQLi works, why ORMs are not enough, and how to prevent it with parameterized queries and defense in depth.

A
Abhishek Patel9 min read

Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

SQL Injection in 2026: Still a Problem, Here's How to Stop It
SQL Injection in 2026: Still a Problem, Here's How to Stop It

The Oldest Vulnerability That Won't Die

SQL injection has been on the OWASP Top 10 since its inception, and it's still in the top 3 in 2026. That's not because we lack defenses -- parameterized queries have existed for decades. It's because developers keep finding new ways to concatenate user input into SQL strings. Every year, major breaches trace back to SQL injection: MOVEit in 2023, several healthcare systems in 2024, and a payment processor in early 2025. The attack surface has shifted, but the core vulnerability hasn't changed since 1998.

If you write code that touches a database, you need to understand SQL injection deeply -- not just the textbook examples, but the edge cases that slip past ORMs, code reviews, and static analysis tools.

What Is SQL Injection?

Definition: SQL injection (SQLi) is a code injection attack where an attacker inserts malicious SQL statements into input fields or parameters that are incorporated into database queries. When user input is concatenated directly into SQL strings without proper sanitization or parameterization, the attacker can read, modify, or delete data and potentially execute system commands.

How SQL Injection Works

The fundamental problem is mixing data with code. When user input becomes part of a SQL statement, the database can't distinguish between the developer's intended query and the attacker's injected commands.

Classic Example

# VULNERABLE -- never do this
username = request.form['username']
password = request.form['password']
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor.execute(query)

If the attacker enters ' OR '1'='1' -- as the username, the query becomes:

SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = ''

The -- comments out the password check. '1'='1' is always true. The query returns all users, and the attacker logs in as the first user -- usually the admin.

Step-by-Step Attack Flow

  1. Identify injection point. The attacker tests inputs by adding single quotes, SQL keywords, or boolean conditions to see if the application behaves differently.
  2. Determine database type. Error messages, response timing, or behavioral differences reveal whether it's PostgreSQL, MySQL, SQL Server, or SQLite.
  3. Extract data. Using UNION SELECT, subqueries, or blind techniques, the attacker reads table names, column names, and data from any table the database user can access.
  4. Escalate access. With database access, the attacker may read credentials for other systems, modify data, create admin accounts, or in some cases execute OS commands.

Types of SQL Injection

In-Band SQLi (Classic)

The attacker sends the payload and receives the result in the same HTTP response. This includes error-based injection (the database error message reveals data) and UNION-based injection (the attacker appends a UNION SELECT to extract data from other tables).

-- UNION-based extraction: steal all usernames and passwords
' UNION SELECT username, password FROM users --

Blind SQL Injection

The application doesn't display database errors or query results, but the attacker can still extract data by asking true/false questions:

-- Boolean-based blind: check if the first character of the admin password is 'a'
' AND (SELECT SUBSTRING(password, 1, 1) FROM users WHERE username='admin') = 'a' --

-- Time-based blind: if the condition is true, the response is delayed
' AND IF((SELECT SUBSTRING(password, 1, 1) FROM users WHERE username='admin') = 'a', SLEEP(5), 0) --

Blind injection is slower but equally devastating. Tools like sqlmap automate the extraction process, testing hundreds of conditions per second.

Second-Order SQL Injection

This is the one that bites experienced teams. The malicious input is stored safely (properly escaped or parameterized on insert) but then used unsafely in a later query. Example: a user registers with the username admin'--. The registration query is parameterized and works fine. But a password reset function later concatenates the username from the database into a new query:

# The username was stored safely, but now it's used unsafely
stored_username = get_username_from_db(user_id)  # Returns: admin'--
query = f"UPDATE users SET password = '{new_password}' WHERE username = '{stored_username}'"
# Becomes: UPDATE users SET password = 'newpass' WHERE username = 'admin'--'
# This resets the actual admin's password

Watch out: Second-order injection passes most security reviews because the input is handled safely at the boundary. The vulnerability is in the internal code path that trusts data from the database. Every query must be parameterized, even when the input comes from your own database.

Why ORMs Don't Make You Immune

ORMs (Object-Relational Mappers) like SQLAlchemy, Prisma, Django ORM, and ActiveRecord use parameterized queries by default. That's great. But every ORM provides an escape hatch for raw SQL, and developers use them more than they should:

# Django ORM -- safe
User.objects.filter(username=user_input)

# Django ORM -- VULNERABLE raw query
User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{user_input}'")
// Prisma -- safe
await prisma.user.findMany({ where: { username: userInput } });

// Prisma -- VULNERABLE raw query
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE username = '${userInput}'`);

The pattern is consistent: ORMs are safe until you bypass them. Audit every use of raw(), execute(), $queryRawUnsafe(), or any method that accepts a SQL string. These are injection points regardless of what ORM you use.

How to Prevent SQL Injection

1. Always Use Parameterized Queries

This is the primary defense. Parameterized queries (also called prepared statements) separate the SQL structure from the data. The database knows which parts are code and which are values.

# Python (psycopg2) -- parameterized
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))

# Node.js (pg) -- parameterized
await client.query('SELECT * FROM users WHERE username = $1 AND password = $2', [username, password])
// Java (JDBC) -- PreparedStatement
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();

2. Input Validation and Allowlisting

Validate input types and formats before they reach any query. If a field should be an integer, parse it as an integer. If it should match a pattern, validate against that pattern. Reject anything that doesn't conform.

3. Least-Privilege Database Accounts

Your application's database user should have the minimum permissions needed. A read-only API endpoint doesn't need INSERT, UPDATE, or DELETE permissions. If the application never needs to DROP tables, the database user shouldn't have that privilege.

-- Create a read-only user for the API
CREATE ROLE api_readonly LOGIN PASSWORD 'strong-random-password';
GRANT CONNECT ON DATABASE production TO api_readonly;
GRANT USAGE ON SCHEMA public TO api_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO api_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO api_readonly;

4. Web Application Firewall (WAF)

A WAF can block known SQLi patterns before they reach your application. It's a defense-in-depth layer, not a primary defense. WAFs have blind spots and can be bypassed with encoding tricks. Never rely on a WAF as your only protection.

Testing for SQL Injection

ToolTypeCostBest For
sqlmapAutomated scanner (OSS)FreeComprehensive SQLi testing, extraction
Burp Suite ProWeb security scanner$449/yrManual testing with automated scanning
SemgrepStatic analysis (OSS)Free / paid tiersFinding raw SQL in code before deployment
OWASP ZAPWeb security scanner (OSS)FreeAutomated scanning in CI/CD pipelines
Snyk CodeStatic analysisFree tier availableIDE-integrated vulnerability detection

Pro tip: Add semgrep to your CI pipeline with the r/python.django.security.injection.sql and equivalent rulesets. It catches raw SQL usage at code review time, which is orders of magnitude cheaper than finding it in production.

Real-World SQL Injection: What Attackers Actually Do

Textbook SQLi examples show login bypasses. In practice, attackers use SQLi for:

  • Data exfiltration -- Dumping entire databases: customer records, payment info, credentials.
  • Privilege escalation -- Creating admin accounts or modifying role assignments.
  • Lateral movement -- Reading database credentials for other services, or using database-level OS command execution (e.g., xp_cmdshell in SQL Server).
  • Ransomware staging -- Encrypting or deleting database contents and demanding payment.
  • Persistent access -- Inserting backdoor accounts or stored procedures that provide ongoing access.

Frequently Asked Questions

Can SQL injection steal data from other tables?

Yes. Using UNION-based injection, an attacker can append queries that read from any table the database user has access to. If the application connects with a privileged account, the attacker can read every table in the database. This is why least-privilege database accounts matter.

Does using an ORM prevent SQL injection?

ORMs prevent injection in their standard query methods because they use parameterized queries internally. However, every ORM provides raw SQL escape hatches that bypass this protection. Any use of raw SQL methods reintroduces the vulnerability. Audit raw query usage in code reviews.

What is the difference between SQL injection and XSS?

SQL injection targets the database by injecting malicious SQL through application inputs. XSS (Cross-Site Scripting) targets the browser by injecting malicious JavaScript into web pages. SQL injection compromises server-side data; XSS compromises client-side sessions and user interactions. Both exploit insufficient input handling.

Can prepared statements be bypassed?

Properly implemented prepared statements cannot be bypassed because the query structure and data are sent to the database separately. However, developers sometimes misuse prepared statements by concatenating input into the query string before passing it to the prepare function, which negates the protection entirely.

How do I detect SQL injection attempts in my logs?

Look for SQL keywords in request parameters: UNION, SELECT, INSERT, DROP, single quotes, double dashes, and semicolons in unusual places. WAFs log blocked attempts. Database slow query logs may show unusual queries. Set up alerts for queries that reference system tables like information_schema.

Is NoSQL immune to injection attacks?

No. NoSQL databases like MongoDB are vulnerable to NoSQL injection, where attackers manipulate query operators. For example, sending {"username": {"$gt": ""}, "password": {"$gt": ""}} as JSON can bypass authentication. The defense is the same: validate input types and use the database driver's parameterization features.

What is sqlmap and is it legal to use?

sqlmap is an open-source tool that automates SQL injection detection and exploitation. It's legal to use on systems you own or have explicit authorization to test. Using it against systems without permission is illegal in most jurisdictions. It's a standard tool in penetration testing engagements and security audits.

Make It Impossible, Not Just Unlikely

The goal isn't to make SQL injection hard -- it's to make it structurally impossible. Parameterized queries do this at the query level. Least-privilege accounts limit damage at the database level. ORMs help, but only when you don't bypass them. Static analysis catches mistakes before deployment. WAFs provide defense in depth. Layer all of these. SQL injection has persisted for 28 years not because it's sophisticated, but because it only takes one missed parameterization in one endpoint to compromise an entire database.

A

Written by

Abhishek Patel

Infrastructure engineer with 10+ years building production systems on AWS, GCP, and bare metal. Writes practical guides on cloud architecture, containers, networking, and Linux for developers who want to understand how things actually work under the hood.

Related Articles

Enjoyed this article?

Get more like this in your inbox. No spam, unsubscribe anytime.

Comments

Loading comments...

Leave a comment

Stay in the loop

New articles delivered to your inbox. No spam.