SAST for Java and Spring Boot: SpotBugs, SonarQube, and CodeQL
Java and Spring Boot applications carry a distinct set of security vulnerabilities: SQL injection through JPQL, insecure deserialization via Java serialization, XML External Entity (XXE) attacks, and Spring Security misconfigurations. Static Application Security Testing (SAST) catches these before runtime. This guide covers SpotBugs (Find Security Bugs plugin), SonarQube, and CodeQL — the three tools you need for Java.
Java Security Vulnerabilities That SAST Catches
Java has been around long enough to accumulate a rich catalog of security anti-patterns. SAST is uniquely effective because Java is statically typed — tools can trace data flows with high precision.
Top vulnerability classes:
- SQL injection via string concatenation in JPA queries
- Insecure Java object deserialization (RCE via gadget chains)
- XXE — XML parsers with external entities enabled
- Path traversal via
Fileoperations with user input - Hardcoded credentials in properties files
- Spring Security configuration mistakes (CSRF disabled, authentication bypass)
- LDAP injection in directory lookups
Tool 1: SpotBugs + Find Security Bugs
SpotBugs is the successor to FindBugs. Alone it finds general code quality issues. With the Find Security Bugs plugin, it becomes a security scanner.
Maven setup:
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.8.3.1</version>
<configuration>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.13.0</version>
</plugin>
</plugins>
<effort>Max</effort>
<threshold>Low</threshold>
<failOnError>true</failOnError>
<includeFilterFile>spotbugs-security-include.xml</includeFilterFile>
</configuration>
</plugin>Include filter (spotbugs-security-include.xml):
<FindBugsFilter>
<Match>
<Bug category="SECURITY"/>
</Match>
</FindBugsFilter>Run:
mvn spotbugs:check -Dspotbugs.failOnError=trueKey security detectors Find Security Bugs provides:
| Detector | Vulnerability |
|---|---|
SQL_INJECTION_HIBERNATE |
JPQL/HQL string concatenation |
OBJECT_DESERIALIZATION |
ObjectInputStream.readObject() from untrusted input |
XXE_SAXPARSER |
SAXParser without external entity protection |
SPRING_CSRF_PROTECTION_DISABLED |
csrf().disable() in Spring Security config |
HARD_CODE_PASSWORD |
Passwords in source code |
PATH_TRAVERSAL_IN |
new File(userInput) |
COMMAND_INJECTION |
Runtime.exec() with user-controlled strings |
Gradle setup:
plugins {
id 'com.github.spotbugs' version '6.0.9'
}
spotbugs {
effort = 'max'
reportLevel = 'low'
}
dependencies {
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0'
}
spotbugsMain {
reports {
xml.required = true
html.required = true
}
}Tool 2: SonarQube
SonarQube provides continuous code quality and security scanning with a web dashboard, trend tracking, and quality gate integration.
For Java/Spring Boot, SonarQube's Java Security rules cover:
- Injection flaws (SQL, command, LDAP, XPath)
- Cryptographic weaknesses (weak algorithms, ECB mode, insufficient key length)
- Sensitive data exposure (logging secrets, stack traces to client)
- Security misconfiguration (debug mode, permissive CORS)
- Spring-specific:
@RequestMappingwithout method restriction, open redirects
Docker-based local SonarQube:
docker run -d --name sonarqube \
-p 9000:9000 \
-e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
sonarqube:communityMaven scan:
mvn sonar:sonar \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.login=$SONAR_TOKEN \
-Dsonar.projectKey=my-spring-appQuality Gate for security — configure in SonarQube UI or via API:
{
"conditions": [
{
"metric": "new_security_hotspots_reviewed",
"op": "LT",
"error": "100"
},
{
"metric": "new_vulnerabilities",
"op": "GT",
"error": "0"
}
]
}This blocks merges when new security hotspots are unreviewed or new vulnerabilities appear.
CI integration (GitHub Actions):
- name: SonarQube Scan
run: |
mvn -B verify sonar:sonar \
-Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \
-Dsonar.login=${{ secrets.SONAR_TOKEN }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Tool 3: CodeQL for Java
CodeQL's Java support is the strongest of any SAST tool for complex taint analysis. It models Spring MVC request handling — knowing that @RequestParam, @PathVariable, and @RequestBody are user-controlled sources.
What CodeQL traces for Java:
- HTTP parameter → JDBC query (SQL injection through multiple layers)
- HTTP parameter →
ProcessBuilder(command injection) - Deserialized object → dangerous method call
@RequestParam→ file path (path traversal)
GitHub Actions setup:
name: CodeQL Java Security
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
codeql:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: java
queries: security-and-quality
- name: Build
run: mvn -B compile -DskipTests
- name: Analyze
uses: github/codeql-action/analyze@v3
with:
category: /language:javaCodeQL must compile your code — it instruments the build to build its database. Ensure your build succeeds without tests (-DskipTests).
Common Java/Spring Vulnerabilities
SQL Injection via JPQL
// VULNERABLE — SpotBugs SQL_INJECTION_JPA, CodeQL flags taint
@Repository
public class UserRepository {
@PersistenceContext
EntityManager em;
public User findByName(String name) {
// String concatenation in JPQL = SQL injection
return em.createQuery(
"SELECT u FROM User u WHERE u.name = '" + name + "'",
User.class
).getSingleResult();
}
}
// SAFE — parameterized query
public User findByName(String name) {
return em.createQuery(
"SELECT u FROM User u WHERE u.name = :name",
User.class
).setParameter("name", name).getSingleResult();
}Insecure Deserialization
// VULNERABLE — SpotBugs OBJECT_DESERIALIZATION
public Object deserialize(byte[] data) throws Exception {
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(data)
);
return ois.readObject(); // Gadget chain attack possible
}
// SAFER — use a serialization filter (Java 9+)
public Object deserialize(byte[] data) throws Exception {
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(data)
);
ois.setObjectInputFilter(info -> {
if (info.serialClass() != null) {
Set<Class<?>> allowList = Set.of(MyData.class, ArrayList.class);
return allowList.contains(info.serialClass())
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;
}
return ObjectInputFilter.Status.UNDECIDED;
});
return ois.readObject();
}XXE in XML Parsing
// VULNERABLE — SpotBugs XXE_SAXPARSER
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(inputStream); // XXE if input is untrusted
// SAFE — disable external entities
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(inputStream);Spring Security CSRF Bypass
// VULNERABLE — SonarQube SPRING_CSRF_PROTECTION_DISABLED
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // Never do this for browser clients
.authorizeRequests().anyRequest().authenticated();
}
}
// SAFE — CSRF enabled by default, disable only for stateless APIs
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeRequests().anyRequest().authenticated();
}
}Combined Pipeline
# .github/workflows/java-sast.yml
name: Java SAST
on: [push, pull_request]
jobs:
spotbugs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- run: mvn spotbugs:check
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- run: |
mvn -B verify sonar:sonar \
-Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \
-Dsonar.login=${{ secrets.SONAR_TOKEN }}
codeql:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- uses: github/codeql-action/init@v3
with:
languages: java
- run: mvn -B compile -DskipTests
- uses: github/codeql-action/analyze@v3Prioritizing Findings
With three tools running, you'll get overlapping results. Triage them:
- Critical/High from CodeQL: Fix immediately — these are proven taint paths
- SpotBugs SECURITY category: Fix in current sprint
- SonarQube Security Hotspots: Review within 2 weeks — these require human judgment to confirm
- Medium/Low from any tool: Backlog, address in quarterly security sprints
Use SonarQube's quality gate to block PRs that add new critical/high findings. Don't block on hotspots — those require review, not necessarily a fix.
Summary
| Tool | Strength | Integration |
|---|---|---|
| SpotBugs + FSB | Java/Spring patterns, bytecode analysis | Maven/Gradle plugin |
| SonarQube | Dashboard, trends, quality gates, team workflow | Maven plugin + server |
| CodeQL | Taint analysis, multi-step injection chains | GitHub Actions |
All three cover different ground. SpotBugs is fast and catches obvious patterns. SonarQube gives you workflow and dashboards. CodeQL finds the subtle multi-layer injection chains that the others miss. Run them in parallel in CI.