SQL injection
is a web security vulnerability where untrusted user input is inserted directly into a SQL query string, allowing attackers to read, modify, delete, or bypass database operations. In Spring Boot applications, it most commonly occurs when developers concatenate request parameters into JPQL or native queries instead of using
PreparedStatement
bound parameters or named
@Query
placeholders. Despite being documented since 1998, SQL injection in Java applications remains one of the most repeatedly disclosed vulnerability classes in OWASP datasets — because the unsafe pattern is still generated by tutorials, legacy code, and increasingly, AI coding assistants.
SQL injection was first documented in 1998. It appeared on the first OWASP Top 10 in 2003. Injection remains one of the most repeatedly tested and disclosed application security classes in OWASP datasets. I audit production Spring Boot APIs regularly. It is still there. Here is why, and exactly what to do about it.
1. The Persistence of SQL Injection: 2026 Data
Before the code, sit with these numbers. They explain why this article still needs to exist.
The uncomfortable part is not the scale. It is what the numbers imply. Parameterised queries have existed since the 1990s. Every major Java framework warns against string concatenation in SQL. And the vulnerability is still in production code reviewed by experienced engineers in 2026.
The reason is not ignorance. It is that the unsafe pattern is faster to write, frequently autocompleted by AI tools trained on older codebases, and easy to miss in code review when the rest of the code looks correct.
A fintech dashboard had a transaction search endpoint:
"SELECT * FROM transactions WHERE ref = '" + userInput + "'"
. In production for 22 months. Reviewed by two engineers. Merged. Forgotten. A compliance scan caught it — not a security review, not a test suite.
2. How AI Coding Assistants Reintroduce SQL Injection Bugs
This is the part nobody wants to say out loud. AI coding assistants — Copilot, Cursor, ChatGPT — are trained on billions of lines of public code. A significant portion of that code predates modern security practices. When you ask an assistant to write a search endpoint in Spring Boot, there is a real chance it autocompletes a concatenated query because that pattern appears frequently in its training data.
I have seen this repeatedly in code reviews over the past year. A junior engineer accepts a suggestion, the reviewer trusts the tool, the PR merges. Nobody ran the query against a security linter. Nobody sent a payload like
' OR '1'='1
.
Treat AI-generated database code the same way you treat user input: verify it. Add
Semgrep
(
java.sql.injection
rule family) to your CI pipeline. It catches concatenated queries automatically on every PR, before human review.
3. Blind SQL Injection and Second-Order SQL Injection in Java — Real Examples
The classic
' OR '1'='1
attack in a login form is mostly caught by WAFs and input scanners today. The attacks that still succeed in 2026 are subtler.
Second-Order SQL Injection in Java
The payload is stored safely during one operation. It fires during a completely different one. Input validation passes at the entry point — the value looks clean. The injection happens when another query reads it back and uses it without parameterisation.
Second-order SQLi — stored at registration, fires in audit log query
-- Step 1: User registers with username: admin'--
-- Input is correctly sanitised at insert time. No injection yet.
INSERT INTO users (username) VALUES ('admin''--'); -- stored safely
-- Step 2: Admin panel queries audit log using stored username
-- Developer writes this query without considering stored payload content:
String query = "SELECT * FROM audit_log WHERE username = '" + user.getUsername() + "'";
-- Expands to: SELECT * FROM audit_log WHERE username = 'admin'--'
-- The -- comments out everything after it. Attacker controls execution.
The registration endpoint parameterises correctly. The audit query looks safe in isolation. Nobody connects the two code paths. Second-order injection requires parameterised queries everywhere data is used , not just where it enters .
Blind SQL Injection Example — Boolean and Time-Based
No error message. No visible output. Attackers infer database structure from the application's behaviour — response time differences or subtle response body changes. Tools like sqlmap automate this extraction using boolean and time-based inference, reconstructing schema and data without triggering a single visible error.
Blind SQLi — no error needed, behaviour reveals the vulnerability
-- Boolean-based: attacker compares two responses
GET /api/users?id=1 AND 1=1 -- TRUE → normal response
GET /api/users?id=1 AND 1=2 -- FALSE → different response
-- Different responses = injectable, even with no error message shown
-- Time-based: 5-second delay confirms blind injection on MSSQL
GET /api/users?id=1; IF(1=1) WAITFOR DELAY '0:0:5'--
-- Attacker then extracts data character by character using IF conditions
"We suppress error messages" is not a defence. Blind injection does not need your error messages.
OWASP continues to list injection among the most critical application security risks precisely because modern ORM layers do not eliminate unsafe query construction automatically. Source: OWASP A03:2021 — Injection · owasp.org .
4. Spring Boot SQL Injection Vulnerabilities Developers Miss
The assumption that "we use Spring Data JPA, so we are safe" is one of the most dangerous beliefs a Java team can hold. It is partially true. Partial safety breeds complacency.
In plain
JDBC
, the rule is clear:
PreparedStatement
always,
Statement
never.
PreparedStatement
compiles the query structure first, then binds the parameter separately — user input can never alter the query structure regardless of its content. Where teams go wrong is when they move to higher-level abstractions —
JPA Repository
,
EntityManager
, dynamic JPQL,
QueryDSL
— and assume the framework handles it unconditionally.
Real SQL Injection Example in Spring Boot — Native Query Concatenation
Java · VULNERABLE — @Query with nativeQuery=true bypasses Hibernate protection
// BAD — native query with concatenation. Hibernate does NOT protect you here.
@Query(value = "SELECT * FROM users WHERE role = '" + role + "'",
nativeQuery = true)
List<User> findByRole(String role);
// role = "admin' OR '1'='1" → full table dump
// GOOD — named parameter binding. Safe regardless of input content.
@Query(value = "SELECT * FROM users WHERE role = :role", nativeQuery = true)
List<User> findByRole(@Param("role") String role);
How to Prevent SQL Injection in Hibernate — Dynamic JPQL
Java · VULNERABLE — dynamic JPQL string building
// BAD — JPQL is also injectable when built dynamically
public List<User> search(String field, String value) {
String jpql = "SELECT u FROM User u WHERE u." + field + " = '" + value + "'";
return em.createQuery(jpql, User.class).getResultList();
}
// GOOD — JpaSpecificationExecutor (type-safe, no string building)
// GOOD — QueryDSL BooleanExpression (injection structurally impossible)
// BooleanExpression expr = user.name.eq(value); // QueryDSL — cannot inject
Criteria API Misuse — How
cb.literal()
Reintroduces Injection
Java · Criteria API — cb.literal() inlines value as string literal
// BAD — cb.literal() with user input inlines the value directly
Predicate p = cb.equal(root.get("status"), cb.literal(userInput));
// GOOD — cb.parameter() binds separately, same as PreparedStatement
ParameterExpression<String> param = cb.parameter(String.class, "status");
Predicate p = cb.equal(root.get("status"), param);
query.setParameter("status", userInput);
Dynamic Sort Parameter Injection
Across multiple Spring Data REST security audits in 2025 and 2026,
?sort=
and
?filter=
parameters were repeatedly flagged as injection sources. Sort field names were interpolated directly into JPQL rather than validated against an allowlist. The
Spring Data JPA documentation
recommends validating sort fields against permitted property names. Use
QueryDSL
predicates or
JpaSpecificationExecutor
for any dynamic filtering logic.
Before reviewing a complex query for injection risks, formatting it first helps. A well-formatted query makes concatenation points —
+
operators near variables — immediately visible.
LearnHubly's SQL Formatter
structures raw SQL in the browser without setup.
PreparedStatement vs Statement — SQL Injection Side-by-Side Fix
@GetMapping("/users")
public List<User> getUsers(
@RequestParam String role) {
String q = "SELECT * FROM users"
+ " WHERE role = '"
+ role + "'";
return em
.createNativeQuery(q, User.class)
.getResultList();
// role="admin' OR '1'='1"
// → dumps entire table
// → no error shown to caller
}
@GetMapping("/users")
public List<User> getUsers(
@RequestParam
@Pattern(regexp="^[A-Z_]{3,20}$")
String role) {
TypedQuery<User> q = em.createQuery(
"SELECT u FROM User u "
+ "WHERE u.role = :role",
User.class);
return q
.setParameter("role", role)
.getResultList();
// Two layers: validation first,
// then parameterised binding.
}
Bean Validation (JSR 380) rejects malformed input before it reaches the database layer. Parameterised queries ensure that even if validation has a gap, the value is never inlined into the SQL string. One layer failing does not cause a breach. Two layers failing simultaneously is far harder.
5. SQL Injection Prevention Checklist for Java Teams
Use this as a PR review checklist. Every item maps to a real vulnerability pattern from the sections above.
- Zero string concatenation in SQL, JPQL, or native queries — named parameters everywhere Core rule
- Plain JDBC code uses
PreparedStatementexclusively — noStatementwith user input JDBC - Bean Validation (
@Pattern,@Size,@NotBlank) on every user-supplied query parameter JSR 380 - Native
@Query(nativeQuery=true)uses:namedParams— never string building Hibernate - Dynamic JPQL built only via Criteria API or
JpaSpecificationExecutor— no manual string construction Spring Data - Sort/filter parameters validated against an explicit field allowlist before query construction Dynamic queries
- Stored data treated as untrusted when read back — prevents second-order injection Second-order
- Database user has minimum permissions:
SELECT/INSERT/UPDATEonly — noDROP/TRUNCATELeast privilege - Semgrep
(
java.sql.injectionfamily) or SonarQube in CI — catches AI autocomplete regressions automatically CI/CD - Error responses never include raw SQL error text or stack traces Error handling
- WAF rules applied at gateway — as a third layer, never as the only layer Defence in depth
6. SQL Injection Testing Payloads You Should Run Before Release
Reading about SQL injection vulnerabilities is not the same as knowing your endpoints are clean. This is the testing sequence I run before any new API endpoint reaches production.
Step 1 — Format the query, inspect for concatenation
Before sending payloads, format the raw SQL. Concatenation risks that are invisible in compressed one-liners become obvious when the query is structured. A
+
operator next to a variable is a red flag that deserves investigation before any payload test.
Paste your query into a browser-based SQL formatter (such as
the one on LearnHubly
) and scan for
+
operators adjacent to variable names. That is where injection risk lives.
Step 2 — SQL Injection Testing Payloads for REST Endpoints
Send these as JSON field values via any REST client. Watch response codes and bodies carefully. A 500 with SQL error text is a confirmed vulnerability. A 400 with a validation message means your input layer is working. A 200 with an unexpectedly large result set is a red flag.
SQL injection testing payloads — send as JSON body fields
// Test 1 — basic quote (confirms the field reaches SQL)
{ "userId": "1'" }
// Test 2 — tautology (classic table dump attempt)
{ "username": "admin' OR '1'='1" }
// Test 3 — comment injection (bypasses trailing WHERE conditions)
{ "id": "1; --" }
// Test 4 — time-based blind (MSSQL — confirms injectable without error output)
{ "ref": "1'; WAITFOR DELAY '0:0:5'--" }
// Test 5 — URL-encoded variant (bypasses naive text-filter WAFs)
// %27 = ' %20 = space %4F%52 = OR
{ "ref": "1%27%20OR%20%271%27%3D%271" }
A browser-based REST client (such as LearnHubly's REST API Tester ) makes it straightforward to fire these payloads directly at your endpoints without installing Postman or curl locally — useful for quick pre-release checks.
Step 3 — Decode suspicious encoded strings from your logs
Attackers URL-encode payloads specifically to bypass text-matching WAF rules. A rule blocking
'
will not block
%27
. When you see unusual percent-encoded strings in your access logs, decode them before deciding whether they are benign.
Spot
1%27%20OR%20%271%27%3D%271
in your logs? Decode it in a browser-based URL decoder (such as
LearnHubly's URL Decoder
) to reveal
1' OR '1'='1
instantly — useful for log analysis without guessing.
7. FAQ — SQL Injection in Spring Boot
SQL injection in Spring Boot occurs when user input is concatenated directly into a SQL or JPQL query string rather than passed as a bound parameter. The database cannot distinguish between intended query logic and attacker-supplied data when both arrive as a single string. The fix is
PreparedStatement
in plain JDBC, or named parameters (
:param
) in
@Query
annotations and
TypedQuery
calls. Using
JPA Repository
derived query methods is safe by default; the risk returns when you build queries dynamically.
Partially. Standard JPQL with named parameters and Spring Data repository derived methods are safe. Native queries with string concatenation, manually built JPQL strings, and sort/filter parameters passed directly to query construction are all still vulnerable. Hibernate does not protect you from unsafe patterns you introduce manually — and the
@Query(nativeQuery=true)
annotation specifically bypasses several of Hibernate's default safeguards.
Second-order SQL injection stores a malicious payload safely during one database operation, then executes it during a different query in a completely separate code path. Input validation at the entry point passes cleanly — the value looks harmless in isolation. The injection fires when another feature reads that stored value and uses it without parameterisation. The fix: treat all data as untrusted when read back from the database, not only when it arrives from the user.
Blind SQL injection produces no visible error. Attackers infer database content from the application's behaviour — response timing differences (time-based) or logical response changes (boolean-based). Tools like sqlmap automate this extraction. The defence is identical to all SQL injection: parameterised queries. Suppressing error messages reduces information leakage but does not prevent the underlying vulnerability.
Yes — when used correctly.
PreparedStatement
compiles the query structure first, then binds user input as a typed data parameter. The input can never alter the query's logic regardless of its content or encoding. The risk returns only if you concatenate strings
before
passing them to
PreparedStatement
, or bypass it entirely with a
Statement
object. Combined with Bean Validation as a first layer, this is the correct defence for plain JDBC code.
No. A WAF is a third layer — not a substitute for parameterised queries. Attackers routinely bypass WAF text-matching rules using URL encoding (
%27
for
'
), hex encoding, SQL comment variations, and case changes. A WAF rule blocking
OR
will not catch
%4F%52
or
Or
. Parameterised queries are immune to all encoding variants because user input is never inlined into the query string at any point.
Yes, and this is increasingly common. AI assistants are trained on large bodies of public code that includes older insecure patterns. When generating database queries they may suggest string concatenation, particularly for queries framed as "simple" by the prompt. The fix is not to stop using AI tools — it is to run AI-generated database code through a security linter (
Semgrep
java.sql.injection
rule family) before merging, and to test new endpoints with the payloads in Section 6 of this article before release.
8. Security is a Process, Not a Patch
Here is what I want you to take from this. Not the code examples — you probably knew most of them. What matters is the pattern behind why SQL injection in Spring Boot applications persists.
It is not ignorance. Every Java developer has heard of
PreparedStatement
. Every Spring Boot tutorial mentions parameterised queries. The OWASP documentation is free and updated. The information is not the problem.
The problem is the gap between knowing and enforcing. Sprint pressure means a Copilot suggestion gets accepted without scrutiny. A reviewer trusts the framework and approves without testing. A security linter is not in CI because "we'll add it later." A vulnerability that takes 20 minutes to fix survives for 22 months until a compliance audit finds it.
SQL injection remains one of the longest-surviving and most repeatedly disclosed vulnerability classes in modern software precisely because it lives in those gaps — not in the areas teams deliberately protect, but in the ones nobody thought to check.
Four changes that close those gaps:
- Add Semgrep
java.sql.injectionto CI. Runs on every PR automatically. Catches AI autocomplete regressions before human review sees them. - Put the Section 5 checklist in your PR template. Not on a wiki. In the template. Every merge gets the same questions answered.
- Test with real payloads before every release. The payloads in Section 6, sent to every new endpoint. A 500 with SQL error text means you just avoided a post-mortem.
- Teach second-order injection in onboarding. Most engineers have never heard of it. One example changes how they think about stored data permanently.
The
PreparedStatement
API predates most frameworks your team uses today. There is no technical reason for SQL injection to still exist in production in 2026. The only reason it does is that enforcement is optional.
Make it mandatory. — Priya
Run These 3 Checks Before Every Production Release
Format your query · test with real payloads · decode suspicious log entries. All free, browser-based, no install.