SAST for Java and Spring Boot: SpotBugs, SonarQube, and CodeQL

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 File operations 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=true

Key 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: @RequestMapping without method restriction, open redirects

Docker-based local SonarQube:

docker run -d --name sonarqube \
  -p 9000:9000 \
  -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
  sonarqube:community

Maven scan:

mvn sonar:sonar \
  -Dsonar.host.url=http://localhost:9000 \
  -Dsonar.login=$SONAR_TOKEN \
  -Dsonar.projectKey=my-spring-app

Quality 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:java

CodeQL 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@v3

Prioritizing Findings

With three tools running, you'll get overlapping results. Triage them:

  1. Critical/High from CodeQL: Fix immediately — these are proven taint paths
  2. SpotBugs SECURITY category: Fix in current sprint
  3. SonarQube Security Hotspots: Review within 2 weeks — these require human judgment to confirm
  4. 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.

Read more

Start now free