Security Tools
Published on: April 28, 2026
10 min read

SQL Injection in Spring Boot (2026): Real Vulnerabilities, Prevention & Testing

✍️ By Priya Singh (Principal Software Engineer)

Principal Software Engineer

SQL Injection in Spring Boot (2026): Real Vulnerabilities, Prevention & Testing
By Priya SinghSenior Technical Insights
Try the Tool

Ready to put this into practice?

We've built a high-performance REST API Tester specifically for the topics discussed in this article. It's free, secure, and runs entirely in your browser.

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.

A03
OWASP Top 10 Injection category rank — present on every edition since 2003
274K+
NIST NVD entries tagged under injection-related vulnerability classes through 2025
$4.44M
Global average cost of a data breach — 2024

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.

⚠️
Real example from a production audit

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 .

HOW AI REINTRODUCES SQL INJECTION INTO MODERN CODEBASES DEVELOPER "Write me a user search endpoint" → AI assistant autocompletes from old patterns AI SUGGESTS "SELECT * FROM users WHERE name = '" + input + "'" trained on old code ☠ compiles fine locally PR REVIEW Reviewer trusts tool. Looks functional. No Semgrep in CI. No payload test. LGTM 👍 Merged. PRODUCTION Attacker sends: ' OR '1'='1 Full table exposed. 22 months later: audit finds it. ☠
The AI acceptance loop — how an autocomplete suggestion becomes a 22-month production vulnerability.
💡
The fix is not to stop using AI tools

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.
⚠️
Why it survives code review

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);
⚡ Recurring Audit Finding · Spring Data REST 2025–2026

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

✗ UNSAFE — STRING CONCATENATION ✓ SAFE — PREPAREDSTATEMENT BINDING USER INPUT: role = "admin' OR '1'='1" "SELECT * FROM users WHERE role='" + role + "'" INPUT MERGED INTO SQL STRING ← injection point DATABASE EXECUTES MODIFIED QUERY ☠ USER INPUT: role = "admin' OR '1'='1" "SELECT * FROM users WHERE role = :role" SQL STRUCTURE PRECOMPILED FIRST driver compiles query before binding INPUT TREATED AS DATA ONLY — cannot alter query ✅
Left: string concatenation merges input into query structure — injectable. Right: PreparedStatement precompiles structure, binds input separately — input cannot alter logic.
✗ String interpolation — DO NOT SHIP
@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
}
✓ Bean Validation + TypedQuery
@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.
}
Why both layers matter

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 PreparedStatement exclusively — no Statement with 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 / UPDATE only — no DROP / TRUNCATE Least privilege
  • Semgrep ( java.sql.injection family) 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.

URL-ENCODED SQLI — BYPASSES TEXT FILTERS, NOT PARAMETERISED QUERIES ATTACKER SENDS 1%27%20OR%20%271%27%3D%271 WAF sees: no quotes, no OR ✓ allowed bypasses WAF ✗ YOUR API SERVER URL-decodes before DB call result: 1' OR '1'='1 decoded STRING CONCAT → INJECTED ☠ PreparedStatement → safe ✅ encoding irrelevant when using parameters
URL encoding bypasses text-based WAF rules but not PreparedStatement — the parameter is never inlined into SQL regardless of encoding.
🔍

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

What is 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.

Does Hibernate prevent SQL injection automatically?

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.

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

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.

What is blind SQL injection and how is it detected?

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.

Is PreparedStatement enough to prevent SQL injection in Java?

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.

Is a WAF enough to prevent SQL injection?

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.

Can AI coding assistants introduce SQL injection vulnerabilities?

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.injection to 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.

Third-Party Links Disclaimer

This article may contain links to third-party websites, documentation, tools, or services for reference and additional information. These external resources are maintained by their respective owners, and LearnHubly does not control or guarantee their availability, accuracy, security, or content. Please review the terms and privacy policies of third-party websites before using their services.

Priya Singh

Java
Spring Boot
React
APIs

Principal Software Engineer • 15+ Years Experience

Priya Singh is a Principal Software Engineer with 15+ years of experience building scalable applications and developer tools. She specializes in backend architecture, APIs, and performance optimization.