SQL Injection: Why It Persists and How to Prevent It

SQL injection has been a documented attack technique since at least 1998. It has appeared in every edition of the OWASP Top 10 ever published. The fix has been well-understood for almost as long as the vulnerability has. And yet SQL injection sits at number five in the 2025 OWASP Top 10, with more than 14,000 CVEs on record, and it keeps showing up in breach investigations. This is the vulnerability class that refuses to die, and understanding why teaches you more about web security than memorising any checklist.

What SQL Injection Actually Is

An application sends SQL queries to a database to read or write data. If the application builds those queries by concatenating user-supplied strings directly into the SQL, then a user who sends malicious input can change the query’s structure, not just its values. That is SQL injection. The attacker does not need access to the database server, does not need special tools, and does not need any credentials: they need a text field and a few test characters.

The canonical example is an authentication bypass. A login form queries the database like this:

SELECT * FROM users WHERE username = 'INPUT' AND password = 'INPUT'

Submit admin'-- as the username and the query becomes:

SELECT * FROM users WHERE username = 'admin'--' AND password = ''

The double-dash comments out the rest of the line. The password check is gone. The attacker is authenticated as admin without knowing any password. A practitioner encountering this for the first time typically does not believe it will work until they try it.

The Three Attack Modes

The login bypass is in-band injection: the attacker sends the payload and reads the result through the same HTTP response. Union-based injection is the other common in-band variant, using the SQL UNION operator to bolt an attacker-controlled SELECT statement onto the original query and return data from unrelated tables.

Most mature applications suppress database output and errors, which eliminates in-band injection. Against these applications, attackers use blind injection. Boolean-based blind injection changes a query condition and watches whether the application response differs at all. Time-based blind injection injects a conditional pause. A 5-second delay on one variant and no delay on another lets the attacker extract data one bit at a time. This is slow. But automated tools handle it, and it works reliably against applications that surface nothing useful to the browser.

Out-of-band injection forces the database to make an outbound DNS or HTTP request carrying data to an attacker-controlled server. This technique requires the database to have external network access, which makes it less common, but it bypasses response-based detection entirely. It appeared in the 2021 Accellion FTA attacks. There, attackers used SQL injection to retrieve database encryption keys. They then used those keys to exfiltrate files from organisations including the Reserve Bank of New Zealand and the State of Washington.

Why This Keeps Happening

The honest answer is that parameterized queries require slightly more effort than string concatenation, and under deadline pressure developers frequently take the faster path. Legacy codebases carry concatenated SQL written when parameterization was not the default. Tutorials and Stack Overflow examples often show string-based query construction without noting the danger. Copy-paste then propagates the pattern.

ORMs help but do not solve the problem. Frameworks like Hibernate, Django’s ORM, and ActiveRecord generate parameterized queries for their standard interfaces, which covers the common case. They all expose raw SQL execution methods for edge cases. Those methods are vulnerable when developers concatenate input into them. The ORM does not prevent SQL injection. Instead, parameterized queries do, whether the ORM generates them or a developer writes them directly.

The 2025 OWASP ranking confirms the persistence of the problem. Despite decades of documentation and framework support, injection flaws remain in the OWASP top five. They are still among the most commonly found and exploited web vulnerabilities. CVE-2025-1094, a SQL injection flaw in PostgreSQL’s libpq library, was chained against BeyondTrust’s Remote Support platform in early 2025. It eventually led to access at US Treasury Department systems.

The Defences That Actually Work

Parameterized Queries

This is the fix. Not a mitigation, not a workaround: the fix. The developer sends the query structure to the database separately from the data. The database parses the query once, understands its shape, and then binds the data values without ever re-parsing them as SQL. An attacker who injects SQL syntax into user input cannot change the query’s intent. The query is already compiled before the data arrives.

According to the OWASP prevention cheat sheet, prepared statements ensure that an attacker cannot change the intent of a query even if SQL commands are inserted. In Java:

// Vulnerable
String q = "SELECT * FROM orders WHERE id = " + orderId;

// Secure
PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE id = ?");
ps.setInt(1, orderId);

Both achieve the same result. One is safe; one is not. Every major database library supports this. There is no defensible reason to use the unsafe version in new code.

Allow-Lists for Structural Query Parts

Bind parameters handle values but cannot parameterize table names, column names, or sort-order keywords because these are structural SQL syntax. For these inputs, use an allow-list: accept only explicitly pre-approved values and map user input to them in code. A sort parameter that only ever resolves to the string “ASC” or “DESC” cannot carry injection.

Least Privilege

The database account an application uses should hold only the permissions it needs. A product catalogue query needs SELECT on the catalogue table, not CREATE, DROP, or FILE. Least privilege does not prevent injection from reading data the account can read, but it limits privilege escalation. Blocking file write privilege removes the web shell path. Separating read and write accounts limits the damage from a read-only injectable query.

Error Suppression in Production

Error-based injection reads from database diagnostic messages that the application forwards to the browser. Remove this in production. Log errors server-side; return generic messages to users. Closing this channel eliminates the easiest attack class and raises the difficulty of the others by cutting off a useful reconnaissance source.

Static Analysis in CI

Tools like Semgrep ship open-source rules that detect string-concatenated SQL in most languages. Running these in a CI pipeline catches new injection paths before they reach production. It also turns legacy audits from a manual review into a manageable issue list. That is the most cost-effective way to eliminate injection from a large codebase systematically.

Testing Your Own Applications

The manual starting point is a single quote in every input. Submit it in URL parameters, form fields, JSON bodies, and HTTP headers. A database error, a page crash, or a behavioural change indicates a likely injection point. sqlmap automates the follow-up: it detects injectable parameters, identifies the injection type, and demonstrates what an attacker could extract. Running it against a staging environment before a production release is standard practice in application security testing.

The National Vulnerability Database and vendor security advisories are the right place to check for known injection CVEs in third-party components. Most exploitation of SQL injection in the wild is not novel research against zero-days: it is automated scanning against known vulnerabilities in unpatched software.

FAQ

Can a WAF replace parameterized queries?

No. WAFs detect and block many known injection payloads, which reduces exposure to automated scanning and opportunistic attacks. Skilled attackers evade WAFs using encoding, comment-based obfuscation, and other techniques. A WAF is a useful layer; it is not a substitute for fixing the code.

What is second-order SQL injection?

Second-order injection stores malicious data in the database first and exploits it later when that data is retrieved and used in another query without sanitisation. Dynamic scanners miss it because the payload and execution are separated in time. The fix is the same: parameterize every query that uses stored data, not just queries fed by direct user input.

Does HTTPS protect against SQL injection?

No. HTTPS encrypts the transport layer between browser and server. The attacker submitting the injection payload is the legitimate browser session. Encryption has no bearing on whether the server-side query is parameterized.

How do ORMs factor into SQL injection risk?

ORMs generate parameterized queries for standard operations, which reduces risk. They expose raw query execution methods for complex cases, and those methods are vulnerable when developers concatenate input into them. Check every raw query call in ORM-based code: these are the most common residual injection surfaces in framework-based applications.

What is the fastest way to find SQL injection in a legacy codebase?

Static analysis. Tools like Semgrep with SQL injection rule sets scan an entire codebase in minutes and produce a list of potentially vulnerable query constructions. Code review of that list is faster than manual line-by-line auditing and more thorough than black-box scanning, which cannot see every execution path.

Related posts

Virus vs Worm: Why the Propagation Difference Actually Matters

Man in the Middle Attack: Techniques, Real Examples, and Defences

How to Detect a Keylogger on Your System