Code Security

SQL Injection Prevention in 2022: Why It Still Happens and How to Stop It

SQL injection has been the top web vulnerability for over two decades. Modern frameworks help, but they do not make it impossible. Here is what still goes wrong.

Yukti Singhal
Security Researcher
7 min read

SQL injection was first documented in 1998. It made the original OWASP Top 10 in 2003. It is 2022, and it is still one of the most common web vulnerabilities found in production applications. This is not because developers are ignorant. It is because the conditions that enable SQL injection are more nuanced than "use parameterized queries" suggests.

Why SQL Injection Still Exists

The standard advice is correct: use parameterized queries or prepared statements, and SQL injection goes away. But this advice assumes a level of control over query construction that developers do not always have.

Dynamic query requirements. Parameterized queries handle values well. They do not handle dynamic table names, column names, or ORDER BY clauses. When a user can sort by any column, you cannot parameterize the column name. You need a whitelist.

ORM escape hatches. Every ORM provides a way to write raw SQL for queries the ORM cannot express. These raw SQL methods are where injection happens. Developers who have never written a vulnerable query in ORM syntax write vulnerable raw SQL without thinking.

Stored procedures. Stored procedures can contain SQL injection if they use dynamic SQL internally. A parameterized call to a stored procedure that internally concatenates user input into a query string is still vulnerable.

Legacy code. Plenty of applications have codebases that predate modern frameworks. Rewriting every query to use parameterized syntax is a significant project that competes with feature work for engineering time.

Non-SQL injection. NoSQL databases have their own injection patterns. MongoDB's query language accepts JavaScript objects, and a crafted input can modify query logic. LDAP injection, XPath injection, and command injection follow similar patterns.

The Parameterized Query Solution

Parameterized queries separate SQL logic from data. The database driver sends the query structure and the data values separately. The database engine compiles the query structure first, then binds the data values. There is no opportunity for user data to be interpreted as SQL.

Java (JDBC):

String sql = "SELECT * FROM users WHERE email = ? AND status = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, userEmail);
stmt.setString(2, "active");
ResultSet rs = stmt.executeQuery();

Python (psycopg2):

cursor.execute(
    "SELECT * FROM users WHERE email = %s AND status = %s",
    (user_email, "active")
)

Node.js (pg):

const result = await pool.query(
    'SELECT * FROM users WHERE email = $1 AND status = $2',
    [userEmail, 'active']
);

The syntax varies by language and driver, but the principle is the same. Data goes in placeholders. Never in the query string.

Handling Dynamic Identifiers

Column names, table names, and ORDER BY clauses cannot be parameterized in most databases. The safe approach is whitelisting.

ALLOWED_SORT_COLUMNS = {'name', 'created_at', 'email', 'status'}
ALLOWED_DIRECTIONS = {'ASC', 'DESC'}

def build_sort_clause(column, direction):
    if column not in ALLOWED_SORT_COLUMNS:
        raise ValueError(f"Invalid sort column: {column}")
    if direction.upper() not in ALLOWED_DIRECTIONS:
        raise ValueError(f"Invalid sort direction: {direction}")
    return f"ORDER BY {column} {direction.upper()}"

This is more code than concatenation. It is also the only safe approach. Any attempt to sanitize or escape identifiers will eventually miss an edge case.

ORM Safety and Its Limits

ORMs like Hibernate, SQLAlchemy, ActiveRecord, and Prisma generate parameterized queries by default. For standard CRUD operations, they eliminate SQL injection entirely.

The dangers arise at the boundaries:

Raw query methods. Every ORM has them. Model.objects.raw() in Django. sequelize.query() in Sequelize. entityManager.createNativeQuery() in JPA. These bypass the ORM's query builder and its parameterization. Treat every raw query method as a potential injection point.

Dynamic filtering. Building complex WHERE clauses dynamically — especially with OR conditions, LIKE patterns, or JSON queries — sometimes pushes developers toward string concatenation even within the ORM.

Query fragments. Some ORMs allow mixing parameterized fragments with raw SQL. The boundary between safe and unsafe is blurry and easy to cross accidentally.

ORM-specific injection. Some ORMs have had their own injection vulnerabilities. Rails had issues with hash-based query conditions. Hibernate had HQL injection. Using an ORM is not a security guarantee — it is a safer default that can be undermined.

Second-Order SQL Injection

First-order injection happens when user input goes directly into a query. Second-order injection happens when stored data — previously accepted from user input — is later used in a query without parameterization.

Example: A user registers with the username admin'--. The registration code uses parameterized queries, so the username is stored safely. Later, a different part of the application builds a query using the stored username without parameterization. The injection triggers not when the data enters the system, but when it is used later.

Second-order injection is harder to find with automated tools because the injection point and the vulnerable query are in different parts of the application. SAST tools with good interprocedural analysis can sometimes find it. DAST tools almost never can.

Prevention: parameterize every query, even when the data comes from your own database. If it originated from user input at any point, it is untrusted.

Blind SQL Injection

When the application does not display query results or error messages, attackers use blind techniques:

Boolean-based blind. The attacker sends queries that produce different application behavior based on TRUE/FALSE conditions. By asking yes/no questions about the database, they extract data one bit at a time.

Time-based blind. The attacker uses database sleep functions (SLEEP(5) in MySQL, pg_sleep(5) in PostgreSQL). If the response takes 5 seconds longer, the condition was true.

Out-of-band. The attacker uses database features to send data to an external server they control (DNS lookups, HTTP requests from the database). This is database-specific and requires certain features to be enabled.

Blind injection is slower to exploit but equally dangerous. Prevention is the same: parameterized queries stop all forms of SQL injection.

Testing for SQL Injection

Manual testing. Inject payloads like ' OR '1'='1, '; DROP TABLE--, and ' AND SLEEP(5)-- into every input field, URL parameter, cookie, and HTTP header. Watch for error messages, behavioral changes, and timing differences.

Automated scanning. DAST tools like SQLMap, Burp Suite, and OWASP ZAP can automate injection testing. SQLMap is particularly thorough for confirming and exploiting confirmed injection points.

Code review. Search for string concatenation in SQL contexts. Regular expressions like "SELECT.*" \+ or f"SELECT.*{ catch many cases. Look for raw query methods in your ORM.

SAST. Static analysis tools excel at finding SQL injection because data flow from sources to sinks is well-understood for this vulnerability class. This is one area where SAST has relatively low false positive rates.

Defense in Depth

Parameterized queries are the primary defense. Additional layers:

  • Least privilege database accounts. The application's database user should not have DROP, ALTER, or GRANT permissions.
  • Web Application Firewall. A WAF can catch common injection patterns, but should not be the primary defense.
  • Input validation. Validate that inputs match expected formats (email addresses look like emails, IDs are numeric) before they reach the query layer.
  • Error handling. Never expose database error messages to users. They reveal database type, table names, and query structure.

How Safeguard.sh Helps

Safeguard.sh helps prevent SQL injection through supply chain visibility. When your ORM, database driver, or query builder library has a known vulnerability that could enable injection, Safeguard.sh flags it before the code reaches production. The platform tracks every dependency version in your application's SBOM and alerts you when security advisories are published. Combined with policy gates that can block deployments using components with critical vulnerabilities, Safeguard.sh adds a supply chain layer to your SQL injection prevention strategy.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.