SQL injection (SQLi) has been understood and preventable for about two decades, yet it still lets attackers read, change or destroy entire databases through a single unparameterised query. In March 2024 CISA and the FBI issued a Secure by Design Alert after a managed file transfer campaign exploited SQL injection to reach thousands of organisations, and OWASP still counts more than 14,000 SQL injection CVEs in A05:2025 Injection. This guide goes deeper than the summary in the Web Application Attacks Guide: it covers the SQLi variants and what each means for detection, parameterisation in the places it is hardest (identifiers, dynamic SQL, stored procedures, ORMs such as Hibernate), blind, second-order and out-of-band injection, database privilege, detection and testing. It is for developers, application security engineers and database administrators, and it is written from the defender's side, with no injection strings or attack procedures.
Key takeaways
- Parameterise everything that is data. Bind variables keep values out of the SQL text; CISA and the FBI call input sanitisation "brittle" and say it "frequently can be bypassed".
- Identifiers need allowlists, not escaping. Table names, column names and sort order cannot be bound, so map user choices to fixed values; this is where many parameterised applications still fail.
- Stored procedures and ORMs are not automatically safe. Dynamic SQL inside a procedure and string-built HQL or raw ORM queries are injectable; OWASP and CWE-564 say so explicitly.
- Blind and second-order SQLi defeat error hiding. Suppressing database errors does not fix injection; data from your own tables is untrusted when it is concatenated into a later query.
- Least privilege decides the blast radius. One database account per application, no owner or DBA rights, and no outbound network access from the database turn a missed injection into a contained incident.
How SQL injection works and its variants
CWE-89 describes the weakness: "The product constructs all or part of an SQL command using externally-influenced input" without neutralising elements that change its meaning. The database then executes whatever valid SQL results; Microsoft's SQL Server documentation notes that the engine "executes all syntactically valid queries that it receives". OWASP's Web Security Testing Guide (WSTG-INPV-05) groups SQL injection attacks into three classes by how data comes back, and the variants below matter mainly because each leaves different evidence.
| Variant | Class | How it works, conceptually | What defenders see |
|---|---|---|---|
| Union-based SQL injection | In-band | Attacker-controlled query results are appended to the application's own result set | Unusual result sizes, queries touching system catalogue tables |
| Error-based SQL injection | In-band | Database error messages returned to the client leak structure and data | Bursts of syntax and conversion errors from the application account |
| Blind SQL injection (boolean) | Inferential | Data is inferred one condition at a time from differences in page content | High volumes of near-identical requests with small parameter changes |
| Time-based SQL injection | Inferential | Deliberate database delays answer true or false questions when nothing else differs | Response time outliers clustered on one route and client |
| Out-of-band SQL injection | Out-of-band | Database features send results over another channel, usually DNS or HTTP | Outbound DNS or network connections from database servers |
| Stacked SQL injection | Any | Where the driver and database accept several statements in one call, a second statement is added | Writes, schema changes or new logins from a read path |
| Second-order SQL injection | Any | Input is stored safely, then later read back and concatenated into another query | Errors or anomalies in batch jobs and admin functions, not the input form |
| Stored procedure injection | Any | A procedure builds dynamic SQL from its parameters | Calls to EXEC, sp_executesql or EXECUTE IMMEDIATE with unusual arguments |
| ORM injection | Any | String-built HQL, JPQL or raw ORM queries, or field names taken from input | ORM query exceptions in application logs |
Two labels describe exposure rather than technique. Pre-auth or unauthenticated SQL injection is reachable without logging in, which is why it drives mass exploitation: CISA advisory AA23-158A describes CL0P exploiting a "previously unknown SQL injection vulnerability (CVE-2023-34362)" in the MOVEit Transfer web application from 27 May 2023. JSON-embedded SQL injection arrives inside JSON request bodies, as values or as keys, which API code often maps straight onto column names and which perimeter filters may not parse the same way the application does.
Parameterisation done properly
The Secure Coding Guide covers the basic prepared statement pattern. OWASP's Query Parameterization Cheat Sheet states "SQL Injection is best prevented through the use of parameterized queries" and lists the bind APIs for each platform: JDBC PreparedStatement, .NET SqlCommand and OleDbCommand parameters, PHP PDO, Ruby ActiveRecord placeholders, Perl DBI, ColdFusion cfqueryparam and Rust SQLx. Query parameterisation means the SQL text is fixed and values travel separately as bind variables, so the database never parses them as SQL; parameterised SQL also lets the database reuse the query plan. The hard cases are the parts of a query that cannot be bound.
| Construct | Why it goes wrong | Safe pattern |
|---|---|---|
| Values in WHERE, INSERT, UPDATE | String concatenation or formatting | Bind variables, typed where the API allows (Microsoft: the Parameters collection treats input "as a literal value") |
| Table and column names | Identifiers cannot be bound; OWASP calls user-chosen names "a symptom of poor design" | Map each user option to a fixed identifier in code; reject anything not in the map |
| Sort direction and order by column | Text appended to the query | Convert to a boolean or enum and append a constant (ASC or DESC) |
| IN lists of variable length | Developers build the list as text | Generate one placeholder per element and bind each value, or use an array parameter type |
| LIKE searches | Wildcards in input widen the match even when bound | Bind the value and escape the wildcard characters, as Microsoft shows for SQL Server |
| JSON field names mapped to columns | Keys concatenated while values are bound | Allowlist of permitted field names, mapped to fixed columns |
| Client-side query builders | Parameterisation done in the browser or a mobile app | OWASP: "ensure that query parameterization is done server-side" |
As safe pseudocode, an identifier allowlist reads: define a constant map from accepted request values to column names; look up the request value; if it is missing, use the default column or return an error; insert only the mapped constant into the SQL text. Validation still has a role as a second layer: Microsoft advises testing "type, length, format, and range" at every tier, and OWASP A05:2025 calls positive server-side validation useful but "not a complete defense". Escaping is the last resort: the OWASP SQL Injection Prevention Cheat Sheet marks it "STRONGLY DISCOURAGED", and A05:2025 notes that table and column names cannot be escaped safely at all.
Stored procedures and dynamic SQL
Stored procedures protect only when they avoid building SQL from their inputs. OWASP A05:2025 warns that "Even when parameterized, stored procedures can still introduce SQL injection" if they concatenate or execute hostile data, and CWE-89 says stored procedures "do not protect against many others" attacks when built from concatenated input.
- Find dynamic SQL: Microsoft says "You should review all code that calls EXECUTE, EXEC, or sp_executesql"; OWASP adds the equivalents on other platforms, such as EXECUTE IMMEDIATE in Oracle PL/SQL.
- Parameterise inside the procedure: in SQL Server, pass values to sp_executesql as declared, typed parameters rather than concatenating them; in Oracle, use EXECUTE IMMEDIATE with a USING clause so values are bind variables.
- Quote identifiers that must be dynamic: wrap names of securables in QUOTENAME and check them against sys catalogue entries or an allowlist first.
- Size buffers: Microsoft warns that dynamic Transact-SQL assigned to a variable "is truncated if it's larger than the buffer allocated", which can cut off the quoting that protected it. Use large enough variables, or apply QUOTENAME and REPLACE directly inside the executed statement.
- Grant execute only: OWASP notes procedures "require execute rights", and teams sometimes respond by granting db_owner. Grant EXECUTE on the specific procedures and no direct table rights.
ORM injection and SQL injection in Hibernate
Object relational mapping injection (CAPEC-109) is SQL injection one layer up. CWE-564, SQL Injection: Hibernate, a child of CWE-89, covers Hibernate executing "a dynamic SQL statement built with user-controlled input", and OWASP A05:2025 gives an HQL example that "still allows unauthorized data access when user input is concatenated into queries". ORMs parameterise their normal query methods, so ORM injection concentrates in a few places:
- String-built query languages: HQL, JPQL and similar query strings assembled with concatenation. Use named parameters (setParameter) or the Criteria API, as OWASP's cheat sheets show.
- Raw and native query helpers: native SQL methods, raw query and "extra" helpers, and literal fragments in query builders. Each needs bind parameters and a code review flag.
- Dynamic field and sort names: filter and sort parameters mapped straight onto entity properties or columns. The McKinsey Lilli incident is the reference case: according to the researchers, values were parameterised but JSON field names were concatenated into SQL on one of 22 unauthenticated endpoints, and error messages that echoed the field names allowed blind refinement until production data could be read. McKinsey says it patched the endpoints within hours of the report.
- Older or non-standard back ends: legacy applications on Microsoft Access (Jet or ACE) databases have their own SQL dialect, covered by a dedicated WSTG sub-test for MS Access, but the defence is the same: OleDbCommand parameters instead of concatenated strings, and migration off shared file-based databases for anything internet-facing.
Blind, second-order and out-of-band SQLi
These variants matter because they defeat the controls teams add after a first finding.
- Blind SQL injection survives error suppression: CWE-89 recommends returning minimal error details, and that is right, but boolean and time-based techniques infer data from page differences and response times. Hiding errors slows an attacker; only parameterisation removes the flaw.
- Second-order SQL injection hides in trusted data: Microsoft describes "a less direct attack" that "injects malicious code into strings that are destined for storage in a table or as metadata". Values read from your own database, such as user names, saved filters or report definitions, must be bound like request data when used in a later query. Review batch jobs, report builders and admin tools, not just input forms.
- Out-of-band SQL injection needs a network path: it relies on the database server making DNS or other outbound connections. Default-deny egress from database servers and alert on their DNS lookups to external domains.
- Stacked queries turn reads into writes: use drivers and settings that send one statement per call where the platform allows, and run read paths under read-only accounts so an added statement fails.
Least privilege and database hardening
CWE-89 advises: "Run your code using the lowest privileges that are required to accomplish the necessary tasks." OWASP's SQL Injection Prevention Cheat Sheet adds concrete patterns. See the Database Security Guide for platform hardening.
- One account per application and purpose: a login or search path needs read access only; a sign-up path needs insert on specific tables. Never connect as sa, root, postgres, dbo or a db_owner member.
- Views for narrow exposure: expose only the columns a function needs, remembering that queries against views still need parameterisation.
- Disable dangerous features: CWE-89 notes that MS SQL includes a built-in function that executes operating system commands, which makes injection far worse; keep xp_cmdshell, OLE automation and external script execution disabled unless a reviewed use case exists.
- Restrict the database service account: run the database engine under a low-privilege operating system account, as OWASP also advises.
- Treat the application's database credential as a machine identity: store it in a secrets manager, rotate it, and prefer short-lived or managed identities where the platform supports them. See the Service Account Security Guide.
SQL injection detection
MITRE ATT&CK records successful exploitation as T1190 Exploit Public-Facing Application, with procedure examples that include SQL injection and a detection strategy (DET0080) that correlates web requests, application errors and server behaviour. Combine signals from three layers:
| Signal | Source | What it can indicate |
|---|---|---|
| SQL syntax and conversion errors from the application account | SQL Server Extended Events (error_reported), PostgreSQL log_min_error_statement, application exception logs | Error-based probing or a working injection point |
| Queries against system catalogues (information_schema, sys views) from an application account | Database audit logs | Schema enumeration after a successful injection |
| Response time outliers on one route | APM and web server logs | Time-based blind SQL injection |
| Bursts of near-identical requests with small parameter changes | WAF, API gateway and web logs | Boolean blind extraction, often by an automated SQL injection tool |
| Outbound DNS or connections from database servers | DNS resolver and firewall logs | Out-of-band exfiltration |
| Rows returned or exported far above the route's baseline | Database activity monitoring, application metrics | Bulk data theft through a working injection |
| New logins, permission grants or schema changes from an application account | Database audit logs | Stacked queries or escalation |
Automated SQL injection tools produce distinctive volumes and request patterns, which is why WAF logs often show the first probe. Treat that as an early warning, not protection: CWE-89 rates application firewalls as moderate, and the McKinsey case shows a flaw in JSON keys that standard scanners reportedly missed. Send all of it to the SIEM; see the SIEM Guide.
Testing and code review
- Static analysis with taint tracking: CWE-89 rates source code analysers and manual source review as high effectiveness, with the caveat that tools miss custom or third-party SQL APIs. Configure rules for your data access layer, ORM raw helpers and stored procedure calls.
- Dynamic testing: CWE-89 rates database and web application scanners as highly effective. Run them against staging with every API, including JSON and GraphQL bodies and authenticated routes.
- Structured manual tests: use WSTG-INPV-05 and its sub-tests for Oracle, MySQL, SQL Server, PostgreSQL, MS Access, NoSQL, ORM injection and client-side injection, and include second-order paths by following stored data into batch jobs and admin features.
- Reviewer questions: is any SQL text built from variables? Are identifiers mapped from an allowlist? Does any procedure call EXEC or EXECUTE IMMEDIATE? Which account does this path use, and can it write?
Rollout and decision guidance
- Inventory data access: list applications, databases, connection accounts and their rights; flag internet-facing and unauthenticated routes first.
- Find string-built SQL: run static analysis and search for concatenated queries, raw ORM helpers and dynamic SQL in procedures; rank by exposure.
- Fix by pattern in shared code: a data access layer that only accepts parameters, an identifier allowlist helper and a reviewed list of permitted raw queries.
- Cut privileges in a pilot: move one application to per-purpose accounts and execute-only rights, run its regression suite, and keep the old grants scripted so they can be restored quickly if a path breaks.
- Lock down the database host: disable command execution features and default-deny egress, logging first so legitimate flows are found.
- Turn on detection: database error and audit logging to the SIEM, with alerts tuned in monitoring mode for two to four weeks before paging.
- Gate releases: block new string-built SQL in pull requests and accept releases against ASVS 5.0 requirement 1.2.4 (parameterised queries, ORMs or equivalent, including in stored procedures).
- ORM or hand-written SQL: OWASP suggests an ORM for new applications; keep hand-written SQL for performance-critical paths, behind the shared parameterised layer.
- Stored procedures or prepared statements: both are safe when written correctly; choose by team skills and change process, not security.
- Code fix or WAF: fix code you own; use WAF virtual patching for third-party products only until the vendor ships a fix. See the Web Application Firewall Guide.
- Small versus large organisations: small teams should use the framework ORM, a lint rule against string-built SQL and a managed database with least-privilege roles; large ones need a paved-road data layer, central database audit logging and findings tracked by CWE-89 across the portfolio.
Metrics: string-built SQL findings open and median time to fix; share of applications on the approved data layer; share of application database accounts with no owner or DBA rights; share of database servers with default-deny egress; repeat SQLi findings in the same application within 12 months.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| OWASP Top 10:2025 | A05:2025 Injection | CWE-89 and CWE-564 are notable CWEs |
| OWASP ASVS 5.0 | V1.2.4 | Parameterised queries, ORMs or equivalent, including in stored procedures |
| MITRE CWE and CAPEC | CWE-89 (child of CWE-943); CWE-564; CAPEC-66, CAPEC-109 | Weakness and attack pattern tagging |
| NIST SP 800-53 Rev. 5 | SI-10, AC-6, SA-11, AU-12 | Input validation, least privilege, developer testing, audit records |
| CIS Controls v8.1 | 16.1, 16.12, Control 5 and 6 | Secure development, code-level checks, account and access management |
| ISO/IEC 27001:2022 Annex A | A.8.28, A.8.29, A.8.2 | Secure coding, security testing, privileged access rights |
| MITRE ATT&CK v20 | T1190; M1026 Privileged Account Management; M1030 Network Segmentation | Exploitation of public-facing applications and the matching mitigations |
Common failure patterns
- Escaping instead of binding: CISA and the FBI call sanitisation brittle; the MOVEit campaign they referenced affected thousands of organisations.
- Parameterised values, concatenated identifiers: sort, filter and JSON field names left out of the fix, as in the McKinsey Lilli case.
- "We use stored procedures": procedures that build dynamic SQL with EXEC, assumed safe because they are procedures.
- Raw ORM helpers as shortcuts: one native query added for performance becomes the injectable path.
- Hiding errors and calling it fixed: blind and time-based techniques still work.
- Application accounts with owner rights: one injection becomes full database compromise, schema changes and, with command execution features on, a host foothold.
Practitioner checklist
- Bind every value with the platform's parameter API; ban string-built SQL with a lint rule and SAST.
- Map identifiers, sort fields and JSON field names through allowlists; never concatenate them.
- Review every EXEC, sp_executesql and EXECUTE IMMEDIATE; pass values as typed parameters and wrap identifiers in QUOTENAME.
- Use named parameters or the Criteria API in Hibernate and flag every raw or native ORM query.
- Treat data read from your own database as untrusted when it feeds another query.
- Give each application per-purpose database accounts with no owner or DBA rights and execute-only grants on procedures.
- Disable database command execution features and default-deny egress from database servers.
- Log database errors and catalogue queries to the SIEM and alert on application-account anomalies.
- Test with WSTG-INPV-05 and its sub-tests, including authenticated, JSON and second-order paths.
- Track SQLi findings, time to fix and recurrence by application.
Standards and references
- MITRE: CWE-89 Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
- OWASP: A05:2025 Injection
- OWASP Cheat Sheet Series: SQL Injection Prevention Cheat Sheet
- OWASP Cheat Sheet Series: Query Parameterization Cheat Sheet
- CISA and FBI: Secure by Design Alert, Eliminating SQL Injection Vulnerabilities in Software
- Microsoft Learn: SQL Injection
- MITRE: CWE-564 SQL Injection: Hibernate
- CISA: #StopRansomware: CL0P Ransomware Gang Exploits CVE-2023-34362 MOVEit Vulnerability
Related NHI Mgmt Group resources: Web Application Attacks Guide · Secure Coding Guide · Database Security Guide · Service Account Security Guide · Web Application Firewall Guide