vulnerabilitysql-injection-preventionsql-injection-detectionvulnerability-assessmentcybersecurity-best-practicesapplication-security

SQL injection prevention: what scanners miss and attackers exploit

Geert WarmenbolGeert WarmenbolApril 29, 2026
Share:
SQL injection prevention: what scanners miss and attackers exploit

Key takeaways

  • Automated scanners miss second-order SQL injection in roughly 40% of vulnerable environments assessed by Vulnox, because the injection and execution happen in separate requests.

  • ORM frameworks do not eliminate SQL injection risk: raw query escape hatches in SQLAlchemy, Hibernate, and Django ORM introduce injection surfaces that developers treat as safe by association.

  • API endpoints authenticated by JWT or API key are scanned less thoroughly by most teams, yet carry SQLi vulnerabilities at rates comparable to unauthenticated web forms in Vulnox assessment data.

  • Least-privilege database accounts reduce blast radius but do not prevent data exfiltration if the application account has SELECT on tables containing PII.

  • Prepared statements prevent injection at the parameterized layer but do not protect against SQLi introduced at the query construction layer before parameterization runs.

TL;DR

SQL injection is not a solved problem. It is a managed one that most teams are managing poorly because they trust their scanner too much and test their API layer too little. Prepared statements are necessary and not sufficient. ORMs introduce injection paths that feel safe because the framework is doing the work. The gaps that matter now are in second-order execution, authenticated API endpoints, and stored procedure misuse. None of those show up cleanly in automated scans.

The application that passed every scan

A 60-person SaaS company running a B2B analytics platform brought us in after their quarterly automated scan came back clean for the third consecutive quarter. Their DevOps team had parameterized queries in the main application layer and used SQLAlchemy throughout. Their CTO was confident enough to say, in the kickoff call, that SQL injection was basically a solved problem for them. That sentence is almost always a signal.

Turning point:

Manual assessment found three injection points in the first two days. None were in the main application layer. Two were in an internal reporting API that used SQLAlchemy text() for complex aggregation queries. The third was a second-order injection in a user preferences table: the application stored a user-controlled string without sanitization, then later interpolated that string into a raw query during a scheduled job. The scanner had never touched the scheduled job. It ran server-side with no HTTP surface to probe.

Why prepared statements are necessary and insufficient

Prepared statements solve one specific problem: they prevent user-controlled input from altering the syntactic structure of a SQL query at the parameterization boundary. That is the right fix for the right surface. The problem is that injection does not only happen at parameterization boundaries. It happens wherever a string is constructed before parameterization runs, wherever a developer decides that a particular input is safe and passes it without binding, and wherever an ORM provides a raw query escape hatch that developers use for anything the ORM cannot express cleanly.

Example

SQLAlchemy text(), Django ORM raw(), and Hibernate createNativeQuery() all exist for legitimate reasons: complex joins, database-specific functions, performance-critical queries. They are also where injection lives in applications that use ORMs everywhere else. The developer writing a complex reporting query in text() is not thinking about injection. They are thinking about getting the query to return the right data. The mental model is that the ORM is safe, so this must be safe too. That is not how injection works.

Second-order injection is structurally different from first-order. The attacker stores a payload during one operation (registration, profile update, preference save) and the payload executes during a separate operation that retrieves and uses the stored value in a query. Scanners probe request-response pairs. Second-order injection requires tracing data across multiple requests and correlating stored values to later query construction, which is why automated tools miss it consistently.

What the assessment data shows

Assessment base: Vulnox vulnerability assessment data across SMB and mid-market clients, 2023-2024

ORM raw query surfaces as primary injection vector

Across vulnerability assessments of applications that used ORMs as their primary data access layer, the injection findings that survived into manual testing were located inside raw query escape hatches in a pattern consistent enough to be predictable. The development teams had instrumented their ORM usage for security review but had not applied the same scrutiny to the raw query paths because the ORM framing created a false sense of coverage.

Implication:

If your application uses an ORM, your injection review needs to specifically enumerate every raw(), text(), createNativeQuery(), or equivalent call and treat each as a potential injection surface regardless of surrounding context.

Authenticated API endpoints carry SQLi at comparable rates to web forms

A logistics company running a REST API for partner integrations had undergone two external scans in the preceding 18 months. Both scans were configured to test unauthenticated surfaces. The authenticated API layer, which required an API key, had never been scanned. Manual assessment found two blind SQLi vulnerabilities in filter parameters that accepted user-controlled sorting and date range inputs. The assumption was that customers with API keys were trusted.

Implication:

Authentication is not a trust boundary for injection testing. An authenticated attacker with a stolen API key, a malicious insider, or a compromised partner system has the same injection surface as the unauthenticated web form. Scan configuration needs to account for this explicitly.

Second-order injection in scheduled jobs and background workers

The SaaS platform described in the opening section is not an isolated case. In assessments where we traced stored user input through application logic into background processing, injection surfaces that scanners cannot reach by definition appeared in roughly a third of environments that had otherwise adequate parameterization on the main web layer. The jobs themselves were often legacy components written before the ORM was adopted.

Implication:

Background workers, scheduled jobs, and batch processors need injection review on the same basis as web-facing code. The absence of an HTTP surface does not mean the absence of injection risk. It means the scanner cannot reach it.

Error handling configurations that confirm injection points to attackers

A financial services client had parameterized queries on all primary paths. What they had not configured was database error suppression in the application layer. Malformed inputs to a secondary search feature returned raw database error strings in the HTTP response, including table names and column references. An attacker does not need to extract data through a live injection to map a schema. They need a verbose error handler and a few minutes.

Implication:

SQL injection prevention is not only about query construction. Verbose error responses give attackers the reconnaissance they need to target injection attempts precisely. Suppress database error messages in production and log them internally.

The parameterized application that was easier to exploit

Common belief

Teams that have adopted prepared statements across the application have effectively addressed SQL injection risk. The argument is reasonable: parameterization prevents the most common injection mechanism, and if the codebase is consistent, coverage should be high.

What we found

In assessments of applications with documented parameterization standards, the injection findings that survived to manual testing were almost uniformly at the edges: migration scripts, admin utilities, reporting functions, and background workers. The main application layer was clean. The code that nobody audits because the injection problem is considered solved was not.

The problem is that consistent parameterization creates a specific cognitive trap. When a developer is confident that their query layer is safe, they are less likely to treat edge cases with the same suspicion. The raw query written for a one-off report does not get the same review that new features get. The migration script that interpolates a configuration value directly into a schema-altering query does not get reviewed at all because it runs once. Confidence in the primary control reduces vigilance at the margins, and the margins are where injection survives.

The surfaces most teams are not testing

GraphQL resolvers

GraphQL introduces injection risk at the resolver layer, where user-supplied field arguments are passed to database queries. The flexibility that makes GraphQL useful makes it difficult to enumerate injection surfaces with standard scanners. Introspection queries reveal the schema to an attacker; that schema tells them exactly which fields to probe.

Stored procedure inputs

Stored procedures are often cited as injection prevention controls. They are not inherently safe. A stored procedure that uses EXECUTE or sp_executesql to construct dynamic SQL from its parameters introduces injection at the database layer, not the application layer, which means application-layer parameterization does not protect against it.

HTTP headers and metadata

User-Agent strings, Referer headers, and custom application headers that get logged to a database are injection surfaces if the logging query concatenates rather than parameterizes. These are rarely included in scanner test profiles because they are not primary input vectors. They appear in assessments when the logging infrastructure is reviewed, not the application code.

Search and filter parameters in internal tools

Internal dashboards, admin panels, and reporting tools built quickly for internal users frequently lack the input validation and parameterization applied to customer-facing surfaces. The implicit assumption is that internal users are trusted. Compromise of one internal account opens these surfaces to an attacker.

Where SQL injection goes next

  1. LLM-to-database integrations will produce a new class of SQL injection that bypasses parameterization entirely, because the injection happens in the prompt before any SQL is generated.

    Applications are being built where natural language queries from users are passed to an LLM that generates SQL, which then executes against a database. The parameterization happens at the SQL execution layer. The injection happens at the prompt layer, before SQL is generated, in a space that parameterization was never designed to address. An attacker who can influence the prompt can influence the generated query. The attack surface exists wherever freeform text feeds an LLM that generates database queries.

    Confidence: highA documented, published exploitation of an LLM-to-database query pipeline in a production application within 18 months.
  2. Second-order SQL injection will appear as a named finding category in major vulnerability scanner outputs within three years, driven by breach disclosures that trace back to this specific mechanism.

    Second-order injection is currently underreported because scanners cannot detect it and manual testers do not always trace stored values through background processing. As LLM-assisted code review becomes more common and breach disclosures become more technically specific, second-order injection paths will be named explicitly in post-incident reports. Scanner vendors will respond to demand.

    Confidence: mediumMajor scanner vendors including Burp Suite, Tenable, and Qualys adding dedicated second-order SQLi detection modules with published detection methodology by 2027.

The real reason injection persists

SQL injection has been on the OWASP Top 10 continuously since 2003. Prepared statements have been available in every major language and framework for at least 15 years. The reason injection persists is not that developers do not know about parameterization. It is that security review is applied to new code and not to accumulated code. The injection surfaces that survive in 2025 are not in the primary application paths that get security attention. They are in the code written before the security standard existed, the utility written by someone who left the company, the migration script that ran once and was never deleted, and the reporting query written by a data analyst who did not know they were writing security-relevant code. Treating injection as a coding problem solves the wrong version of the problem.

Counterargument

The counterargument is that if parameterization were applied universally as a coding standard, the accumulated code problem would eventually be resolved through refactoring. That is true in theory. In practice, legacy code does not get refactored unless it causes visible problems, and injection vulnerabilities in background jobs and internal tools do not cause visible problems until they are exploited. The standard is applied to surfaces that get reviewed. The surfaces that do not get reviewed are where the risk lives.

One thing to do this week

Pull a list of every raw query call in your codebase. In Python, search for text() and raw(). In Java, search for createNativeQuery() and createQuery() with string concatenation. In .NET, search for FromSqlRaw() and ExecuteSqlRaw(). Do not filter by file or directory. Include migration scripts, background workers, admin utilities, and scheduled jobs. Put that list in front of whoever owns injection review. If the list is empty, the search pattern is wrong. Every ORM-based application of meaningful size has raw queries somewhere. Finding them is the first step. This week, just find them.

Further Reading

Frequently Asked Questions

Why do automated scanners miss SQL injection vulnerabilities in modern applications?

Automated scanners miss injection surfaces that do not have a direct HTTP request-response pair to probe. Second-order injection, where input is stored and later used in a query by a background job, is invisible to scanners by design. Authenticated API endpoints are frequently excluded from scan scope. ORM raw query escape hatches do not appear as injection surfaces to scanners that evaluate parameterization at the framework level. Vulnox assessment data consistently shows injection surviving in these surfaces after scans return clean results.

Are ORM frameworks like Django or SQLAlchemy safe from SQL injection?

ORMs reduce injection risk on standard query paths but do not eliminate it. Every major ORM provides raw query escape hatches: SQLAlchemy text(), Django ORM raw(), Hibernate createNativeQuery(). These exist for legitimate reasons and are used frequently for complex queries. When developers use them, they leave the ORM injection protection behind. The mental model that the ORM is safe causes developers to apply less scrutiny to raw query calls, which is where injection survives in ORM-based applications.

What is second-order SQL injection and why is it hard to detect?

Second-order SQL injection occurs when user-controlled input is stored in the database during one operation and then retrieved and used in query construction during a separate operation. The scanner probing the storage operation sees no injection because no query executes immediately. The scheduled job or background worker that later uses the stored value has no HTTP surface for the scanner to probe. Detection requires tracing stored values through application logic to their eventual use in query construction, which requires manual code review or dynamic instrumentation.

How should API endpoints be tested for SQL injection?

API endpoints require the same injection testing as web forms, including when they are authenticated. Authentication is not a trust boundary for injection testing: a stolen API key, compromised partner system, or malicious insider has access to the same injection surfaces as an unauthenticated attacker. Scanner configuration should include authenticated sessions for API testing. Filter parameters, sort parameters, search fields, and date range inputs in APIs are common injection surfaces that receive less scrutiny than primary data submission endpoints.

What SQL injection surfaces do most development teams overlook?

Overlooked surfaces in Vulnox assessments include ORM raw query calls in reporting functions, HTTP headers logged to databases, stored procedure inputs that construct dynamic SQL internally using EXECUTE or sp_executesql, GraphQL resolver parameters, internal admin tools and dashboards, migration scripts that interpolate configuration values into schema-altering queries, and background workers that retrieve user-controlled stored values and use them in query construction.

Does enforcing prepared statements across a codebase solve the SQL injection problem?

Prepared statements solve injection at parameterization boundaries on audited code paths. They do not address injection in raw query calls written before the standard was adopted, migration scripts, admin utilities, or background workers that developers do not associate with user-controlled input. Confidence in parameterization on primary paths tends to reduce scrutiny of edge-case code, which is where injection survives. Effective prevention requires enumerating every raw query call in the codebase, not just verifying that the ORM is in use.

Related Articles

API security assessment: what automated tools miss and manual testing finds

API security assessment: what automated tools miss and manual testing finds

Automated API security tools find the vulnerabilities they were designed to look for. The findings that matter in real assessments, broken object-level authorization, JWT algorithm confusion, and unauthenticated internal endpoints, require a tester who understands what the API is supposed to do before probing what it actually does.

CVSS score explained: what the metric captures, what it misses, and how to fix your prioritization

CVSS score explained: what the metric captures, what it misses, and how to fix your prioritization

In 40% of Vulnox assessments, vulnerabilities scored CVSS 9.0 or higher were on systems with compensating controls that materially reduced the actual risk. The score was accurate. The priority order it implied was not. This guide covers how to use CVSS correctly and what to layer on top of it.

Post-breach vulnerability assessment: what the attacker left behind

Post-breach vulnerability assessment: what the attacker left behind

Most post-breach assessments start too late and end too early. They confirm what was hit, not what the attacker mapped before they moved. The second breach usually comes from the reconnaissance that happened in the first.

Ready to Secure Your Digital Assets?

Get a comprehensive vulnerability assessment for your website today.