SQL injection has been in OWASP's Top 10 web application vulnerabilities continuously since that list was created in 2003. It appeared in the 2021 version, ranked third under the broader "Injection" category. In that time it has been responsible for some of the largest data breaches in history, been the attack vector for state-sponsored intrusions, powered the takedowns of corporate websites by hacktivist groups, and enabled individual researchers to exfiltrate databases from thousands of vulnerable sites. Despite 25 years of awareness, SQL injection remains one of the most common and impactful vulnerability classes in web applications.
The fundamental concept is simple: a web application accepts user input, incorporates that input into a SQL query without adequate sanitization, and sends the result to a database. The attacker's input is interpreted as SQL code rather than as data. The consequence can range from retrieving data the attacker was not authorized to see, to modifying or deleting data, to executing operating system commands on the database server. The simplicity of the concept is part of why it persists: it can be introduced by any developer who builds a query string through concatenation without thinking about what happens if the user enters a quote mark.
The 1998 Origins
SQL injection was formally documented by Jeff Forristal (writing under the handle rfp) in a Phrack article in 1998, though the vulnerability class was likely being exploited before that. Forristal's article, "NT Web Technology Vulnerabilities," described how user input incorporated into database queries could be manipulated to alter query semantics. The example was Visual InterDev and SQL Server, which was the dominant web development stack for Windows-based applications at the time.
The article introduced the core techniques that remain relevant: using single quotes to terminate string literals and inject additional SQL syntax, using comment sequences (-- or /*) to truncate the remainder of the query, using UNION SELECT to retrieve data from other tables, and using blind techniques to extract information when error messages are suppressed. The vulnerability class was named and documented with enough specificity that developers could recognize and fix it - and enough specificity that attackers could exploit it systematically.
The initial response from the software industry was slow. Microsoft's SQL Server ran by default with an account that had high operating system privileges (the SA account), and many applications were deployed with those privileges rather than least-privilege database accounts. This meant that SQL injection in an early web application could often be escalated to command execution on the server via SQL Server's xp_cmdshell stored procedure.
Classic SQLi Techniques
In-band SQL injection - where results are returned directly in the application's HTTP response - is the simplest form. A login form that builds the query SELECT * FROM users WHERE username = '[input]' AND password = '[input]' can be bypassed by entering ' OR '1'='1 as the username, transforming the query to SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '' - which returns all users because '1'='1' is always true.
UNION-based injection allows retrieving data from other tables. If the application displays query results, an attacker can append UNION SELECT null,username,password FROM admin_users-- to retrieve credentials from an administrative table. The column count must match the original query, and data types must be compatible - determining these through trial and error or information schema queries is a standard exploitation step.
Error-based injection uses database error messages to extract information. When a query fails, many database engines return descriptive errors that include the value that caused the failure. Injecting constructs that force type conversion errors (such as converting a string column to an integer) can cause the database to include the column value in the error message. Developers who return raw database error messages to users enable this technique.
Blind SQL injection is used when the application does not return query results or error messages, but does behave differently based on whether a query returns true or false. Boolean-based blind injection asks yes/no questions: if the first character of the admin password is 'a', render normally; if not, return an error. By systematically asking these questions, the attacker can extract arbitrary data character by character - slowly, but reliably. Time-based blind injection uses deliberate delays (SLEEP() in MySQL, WAITFOR DELAY in SQL Server) as the signal rather than content differences, making it work even against applications that behave identically for true and false queries.
Major Historical Exploits
CardSystems Solutions, 2005: SQL injection against this payment processor exposed 40 million credit card numbers - one of the first major financial breaches to receive public attention. The Federal Trade Commission's investigation found that CardSystems had been storing full cardholder data (a PCI violation) and had left the SQL injection vulnerability unpatched despite a security audit identifying it. CardSystems went out of business as a direct result.
Heartland Payment Systems, 2008: Albert Gonzalez and co-conspirators used SQL injection to breach Heartland, then the fifth-largest payment processor in the US, and install malware that captured card data from the company's network. Roughly 130 million cards were compromised. Gonzalez had previously used SQL injection against TJX, Dave and Buster's, and multiple other retail targets. His combined sentence for these breaches was 20 years - the longest sentence ever given for a hacking offense in the US at the time.
LulzSec's 50-day rampage in 2011 included multiple SQL injection attacks: HBGary Federal (which had also been compromised via SQLi), Sony Pictures (1 million user records), the Arizona Department of Public Safety, and others. The attacks demonstrated that SQL injection was not only a sophisticated APT technique but one accessible to teenagers with basic technical knowledge and Havij (an automated SQLi tool).
Cl0p's MOVEit attack in 2023 - the largest ransomware-adjacent incident of that year, affecting 2,700 organizations - used a SQL injection zero-day in MOVEit Transfer software. The vulnerability was CVE-2023-34362, an unauthenticated SQL injection flaw that allowed remote code execution. Cl0p had apparently tested the vulnerability as early as 2021 and waited to deploy it at mass scale. The attack demonstrated that SQL injection remains relevant as an initial access vector against enterprise software, not just custom web applications.
Modern Bypass Techniques
Web Application Firewalls filter SQL injection attempts using signature detection. Attackers have developed extensive bypass techniques. Case variation exploits WAFs that match case-sensitively: SeLeCt instead of SELECT. Encoding uses URL encoding, double URL encoding, or Unicode encoding of key characters. Comment insertion breaks up keywords: SEL/**/ECT or SE+LECT. Whitespace variation uses tabs, newlines, or other whitespace in place of spaces, which SQL parsers accept but pattern matchers may not.
Second-order SQL injection - where the injected payload is stored, then executed when retrieved in a later query - bypasses WAFs that only inspect at input time. An attacker creates an account with username ' OR 1=1--, which is stored safely. When the application later uses that username in a query (password change, profile display) without re-sanitizing the retrieved value, the injection executes.
Out-of-band SQL injection exfiltrates data via DNS or HTTP requests from the database server rather than through the HTTP response. DNS-based exfiltration using SQL Server's master..xp_dirtree or MySQL's LOAD_FILE can send database contents as DNS lookup labels to an attacker-controlled domain. This bypasses restrictions that prevent error messages or results from appearing in HTTP responses, and also bypasses network monitoring that focuses on HTTP traffic.
NoSQL injection is the equivalent vulnerability class for non-relational databases. MongoDB, CouchDB, and similar databases accept JSON or structured queries rather than SQL, but they can be manipulated in equivalent ways. Injecting MongoDB operators like $ne (not equal) or $gt (greater than) into query parameters can bypass authentication or retrieve unauthorized data. The vulnerability class is distinct from SQL injection but shares the same root cause: interpreting user input as query structure rather than query data.
Defenses and Why They Fail
Parameterized queries (prepared statements) are the definitive defense. A parameterized query separates the SQL structure from the user input: the query is compiled with placeholders, and user-supplied values are bound to the placeholders without ever being interpreted as SQL syntax. A parameterized login query would be SELECT * FROM users WHERE username = ? AND password = ?, with the user input bound to the ? placeholders afterward. No matter what the user enters, it is treated as a literal string value, not as SQL code.
Stored procedures provide similar protection when implemented correctly - but not when the stored procedure itself builds dynamic SQL through concatenation internally. An application that calls a stored procedure with parameterized arguments, but where the stored procedure then does string concatenation to build EXEC(@sql), has transferred the vulnerability to the database layer rather than eliminated it.
Input validation and sanitization as the primary defense is historically unreliable. Developers who attempt to blacklist dangerous characters inevitably miss edge cases. Encoding and bypass techniques exist for virtually every blacklist. Input validation as a defense-in-depth layer is useful; as the primary defense it has failed repeatedly.
ORM (Object-Relational Mapping) frameworks - SQLAlchemy, Hibernate, ActiveRecord, Django ORM - generate parameterized queries automatically, removing the need for developers to write SQL directly. ORMs have substantially reduced SQL injection incidence in applications built on mature frameworks. But raw query execution remains available in all ORMs and is frequently used for complex queries, performance-sensitive operations, or by developers who do not understand that parameterized query generation is the protective mechanism they should not bypass.