https://github.com/gabrielbbaldez/spring-taint
Interprocedural taint analysis for Spring Boot, built on Tai-e — detects multi-layer data-flow vulnerabilities that SonarQube can't reach.
https://github.com/gabrielbbaldez/spring-taint
java sarif sast security spring-boot static-analysis tai-e taint-analysis
Last synced: about 1 month ago
JSON representation
Interprocedural taint analysis for Spring Boot, built on Tai-e — detects multi-layer data-flow vulnerabilities that SonarQube can't reach.
- Host: GitHub
- URL: https://github.com/gabrielbbaldez/spring-taint
- Owner: GabrielBBaldez
- License: mit
- Created: 2026-06-15T23:23:28.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-16T02:17:50.000Z (about 1 month ago)
- Last Synced: 2026-06-16T02:20:38.860Z (about 1 month ago)
- Topics: java, sarif, sast, security, spring-boot, static-analysis, tai-e, taint-analysis
- Language: Java
- Size: 105 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
Awesome Lists containing this project
README
# Spring Taint Analyzer
[](https://github.com/GabrielBBaldez/spring-taint/actions/workflows/ci.yml)
[](https://github.com/GabrielBBaldez/spring-taint/releases)
[](LICENSE)
[](#building)
> Interprocedural taint analysis for Spring Boot applications, built on [Tai-e](https://github.com/pascal-lab/Tai-e).
> Detects multi-layer data-flow vulnerabilities that conventional tools such as SonarQube cannot reach.
Detects **12 vulnerability classes** across **6 frameworks**, including cross-layer,
reactive, cross-service, and cross-request stored injection — **33/33 vulnerable
benchmark cases with 0 false positives**, plus a near-miss layer that flags
*attempted-but-incorrect* sanitization. Ships as a CLI, a self-contained jar, a
Docker image, and a GitHub Action with SARIF 2.1 output.
---
## The problem
Consider this seemingly harmless Spring Boot code:
```java
// Controller
@GetMapping("/users")
public List search(@RequestParam String name) {
return userService.search(name);
}
// Service
public List search(String name) {
String filtered = nameFilter(name); // looks like sanitization, but isn't
return userRepo.findByName(filtered);
}
// Repository
public List findByName(String name) {
return jdbc.query(
"SELECT * FROM users WHERE name = '" + name + "'", // 🚨 SQL Injection
mapper
);
}
```
The value comes from `@RequestParam`, crosses the service and repository layers, and reaches a SQL query without sanitization. A trivial payload like `name = ' OR '1'='1` exposes the whole table.
**SonarQube does not detect this path.** It only flags the case where the sink is in the same method as the source. The real vulnerability lives in flows that cross multiple layers — and that is exactly where this project operates.
---
## What is taint analysis
Taint analysis tracks the flow of untrusted data through a system using three concepts:
```
[SOURCE] ──► data flow ──► [SANITIZER?] ──► [SINK]
│
if absent → alert
```
- **Source** — where external data enters: `@RequestParam`, `@RequestBody`, `@KafkaListener`
- **Sanitizer** — what cleans the data: `HtmlUtils.htmlEscape()`, parameterized queries, `@Valid`
- **Sink** — where dangerous data is consumed: `JdbcTemplate.execute()`, `Runtime.exec()`, `response.write()`
If data flows from a source to a sink **without passing through a sanitizer** → potential vulnerability.
The analysis is **interprocedural**: it tracks data across methods, classes, and abstraction layers — not just within a single function.
---
## Positioning: complementary to SonarQube
This project does **not** replace SonarQube. They serve different purposes:
| Tool | Purpose | Interprocedural taint |
|---|---|---|
| SonarQube | General quality + bugs + simple vulnerabilities | ❌ |
| Semgrep OSS | Static code patterns | ❌ |
| Semgrep Pro | Interprocedural taint | ✅ — but **paid** |
| Checkmarx / Veracode | Full enterprise SAST | ✅ — but **expensive** |
| **Spring Taint Analyzer** | Interprocedural taint for Spring Boot | ✅ — **free** |
Where it differs in practice — the Spring-specific capabilities that depend on real
interprocedural taint:
| Capability | Spring Taint | SonarQube (free) | Semgrep OSS |
|---|:---:|:---:|:---:|
| Interprocedural taint (across methods/layers) | ✅ | ❌ | ❌ |
| `@KafkaListener` / `@FeignClient` as sources | ✅ | ❌ | ❌ |
| `MultipartFile` / `@MatrixVariable` as sources | ✅ | ❌ | ❌ |
| Conditional sanitizers | ✅ | ❌ | ❌ |
| Cross-request stored injection | ✅ | ❌ | ❌ |
| WebFlux / Reactor (`Mono` / `Flux`) | ✅ | ❌ | ❌ |
| JPQL / template / JNDI / XXE injection | ✅ | ❌ | ❌ |
| Spring Security & `application.yml` misconfig | ✅ | ❌ | ❌ |
| Near-miss sanitizer detection (wrong/insufficient sanitization) | ✅ | ❌ | ❌ |
| Autofix — applies a parameterized-query patch | ✅ | ❌ | ❌ |
| Per-finding confidence score | ✅ | ❌ | ❌ |
| Diff mode for pull requests | ✅ | ❌ | ✅ |
| SARIF 2.1 output | ✅ | ✅ | ✅ |
| Free / open source | ✅ | ✅ | ✅ |
> **Semgrep Pro** has interprocedural taint but is a paid product (~$35/dev/month).
> Spring Taint Analyzer delivers the Spring Boot–focused equivalent, free.
Expected use in a CI pipeline:
```yaml
- sonarqube scan # general quality, code smells, coverage
- spring-taint scan # deep data-flow vulnerabilities
```
**Value proposition in one sentence:** you already use SonarQube — this project detects what it cannot see.
---
## Architecture
Built on **[Tai-e](https://github.com/pascal-lab/Tai-e)** (Nanjing University, ISSTA 2023), a modern static-analysis framework for Java. Tai-e solves the hard parts — call-graph construction, context-sensitive pointer analysis, and interprocedural **IFDS** taint propagation. Our work is the Spring layer on top of it.
```
Spring Boot project
│ compile (Maven / Gradle)
▼
Bytecode (.class / JAR) ← analysis runs here, not on source
│
▼
Tai-e: Call Graph + Pointer Analysis
│
▼
IFDS Taint Propagation
│
▼
Spring Source/Sink Config ← our differentiator
(@RequestParam, @KafkaListener,
JdbcTemplate, Runtime.exec…)
│
▼
SARIF 2.1 report
(terminal / GitHub / GitLab / VS Code)
```
Operating on bytecode (not source) gives precise inheritance/generics resolution, analysis of third-party dependencies without source, and independence from any IDE or build system.
---
## Project layout
```
spring-taint/
├── config/
│ └── spring-taint.yml # default Spring sources/sinks/sanitizers (Tai-e format)
├── docs/
│ └── design/ # technical scope & design notes
├── spring-taint-engine/ # analyzer: CLI, config loader, Tai-e adapter, SARIF reporter
└── spring-taint-benchmark/ # intentionally vulnerable Spring Boot cases + ground truth
```
---
## Benchmark
Like FlowDroid's DroidBench, this repo ships a benchmark of intentionally vulnerable (and intentionally safe) Spring Boot cases. Every advertised detection is validated against it before release.
The benchmark has **37 cases (34 vulnerable, 3 safe)** across SQL and JPQL injection
(direct, through-service, four-layer, via-Kafka, reactive R2DBC), reflected,
conditional-sanitizer and **cross-request stored** XSS, SSRF, SpEL, JNDI, XXE,
template injection (SSTI), log injection, path traversal, command injection, and
open redirect — with sources from Spring (`@RequestParam`, `@PathVariable`,
`@RequestBody`, `@RequestHeader`, `@MatrixVariable`, `MultipartFile`),
`@KafkaListener`, JAX-RS (`@QueryParam`), `@Repository` reads, **`@FeignClient`
results, `@Scheduled` jobs and `@Transactional` write-then-read**, plus taint
flowing through `Optional` / `CompletableFuture` wrappers. Ground truth is
in [`expected.yml`](spring-taint-benchmark/expected.yml).
Current engine result: **33/33 vulnerable cases detected, 0 false positives** on the
3 safe cases; the near-miss layer (`--src`) catches one further wrong-context flow
(34) and explains the rest. Full table: [benchmark README](spring-taint-benchmark/README.md).
Per-rule reference: [docs/rules.md](docs/rules.md).
Positive cases measure **recall**; safe cases measure **precision**.
---
## Scope by phases
- **Phase 1 — Spring MVC (MVP):** SQL injection, XSS, path traversal, command injection, SSRF, SpEL injection, open redirect. Sources: `@RequestParam`, `@PathVariable`, `@RequestBody`, `@RequestHeader`, `@CookieValue`, `@ModelAttribute`, servlet API. Exit criterion: detect every benchmark case with zero false negatives and precision > 80%.
- **Phase 2 — gaps left open by existing OSS tools (done):** `@KafkaListener` as a source, conditional sanitizers, custom method sanitizers, cross-request stored injection, WebFlux / async (`Mono`/`Flux` as transparent taint wrappers).
- **Phase 3 — multi-framework & robustness (done):** JAX-RS / Quarkus and Micronaut sources; JNDI / XXE / template / JPQL / log-injection sinks; `@FeignClient`, `@Scheduled` and `@Transactional` sources; configuration and misconfiguration audits.
- **Phase 4 — roadmap:** gRPC and RabbitMQ sources, an IntelliJ plugin, and publishing the image to GHCR.
The full technical scope lives in [`docs/design/spring-taint-scope.md`](docs/design/spring-taint-scope.md).
---
## Usage
The commands (the runnable `java -jar …/spring-taint-all.jar` form is in
[Building](#building); `scan` needs the target's dependency classpath via `--libs`):
```bash
# Basic scan
spring-taint scan target/classes --libs ""
# Custom configuration (merged onto the built-in rules)
spring-taint scan target/classes --libs "…" --config spring-taint.yml
# SARIF output (GitHub Advanced Security, GitLab SAST, VS Code)
spring-taint scan target/classes --libs "…" --output results.sarif
# Filter by severity, show the full trace
spring-taint scan target/classes --libs "…" --severity critical,high --verbose
# Only findings touching files changed vs a base ref (fast PR scans)
spring-taint scan target/classes --libs "…" --diff origin/main
# Near-miss notes + suggested parameterized-query fixes (needs the sources)
spring-taint scan target/classes --libs "…" --src src/main/java --suggest-fixes
# Apply the high-confidence fixes to the source
spring-taint scan target/classes --libs "…" --src src/main/java --fix
```
Example output (every taint finding carries a confidence score):
```
[CRITICAL] sql-injection (confidence: 95%)
Source: UserController.java:28 - search() - tainted parameter
Flow: UserController.search() -> UserService.search() -> UserRepository.query()
Sink: UserRepository.java:27 - sink: query()
Sanitizer: none detected
```
---
## Extensibility
Teams can add their own rules in Tai-e's YAML format:
```yaml
sources:
- { kind: call, method: "", index: result }
- { kind: param, method: "", index: 0 }
sinks:
- { method: "", index: 0 }
sanitizers:
- { method: "", index: 0 }
```
---
## Building
Requires JDK 17+ and Maven.
```bash
mvn -q clean package # build engine + benchmark
mvn -q -pl spring-taint-benchmark package # compile the benchmark cases only
```
### Running a scan
Build the self-contained jar and scan compiled classes. Pass the target's
dependency classpath with `--libs` so framework types like `JdbcTemplate` resolve
(the taint config is bundled, so `--config` is optional):
```bash
java -jar spring-taint-engine/target/spring-taint-all.jar \
scan target/classes --libs "$(... your dependency classpath ...)" \
--output results.sarif
```
> **The analysis currently runs on JDK 17.** Tai-e 0.5.1's frontend cannot read
> JDK 21 bytecode, so run the analyzer with a JDK 17 runtime (the project still
> compiles to Java 17 on any JDK 17+).
A custom `--config` is **merged** onto the built-in rules (use `--no-default-config`
to replace them instead).
### Secrets, configuration and misconfiguration scans
Three pattern-based scans (any JDK, no taint engine) complement the taint analysis:
```bash
# Hardcoded secrets in bytecode — secret-named constants, known key formats
# (AWS, GitHub, …), and @Value defaults
java -jar …/spring-taint-all.jar secrets target/classes
# Insecure settings in application*.yml / .properties — hardcoded secrets,
# disabled TLS, Security auto-config excluded, Actuator "*", H2 console
java -jar …/spring-taint-all.jar config src/main/resources
# Insecure Spring code in bytecode — csrf()/frameOptions().disable(),
# @CrossOrigin("*"), insecure cookies, sensitive data logged
java -jar …/spring-taint-all.jar misconfig target/classes
```
---
## GitHub Action
Run the analyzer in CI and upload findings to GitHub code scanning. The action
runs as a Docker container on JDK 17; give it the compiled classes and the
dependency classpath:
```yaml
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '17' }
- run: mvn -B -ntp package -DskipTests
- id: cp
run: echo "value=$(mvn -q dependency:build-classpath -Dmdep.outputFilterFile=/dev/stdout)" >> "$GITHUB_OUTPUT"
- name: Spring Taint Analysis
uses: GabrielBBaldez/spring-taint@main
with:
path: target/classes
libs: ${{ steps.cp.outputs.value }}
output: results.sarif
severity: critical,high
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results.sarif
```
See [`action.yml`](action.yml) for all inputs. This repo also scans its own
benchmark on every push — see [`.github/workflows/ci.yml`](.github/workflows/ci.yml).
---
## Dashboard
A web console (React + Vite + TypeScript) visualizes the SARIF output: severity
breakdown, findings by rule, and the full **source → sink taint flow** for each
finding. Drop a `.sarif` file to load your own report. See
[`dashboard/`](dashboard/).
```bash
cd dashboard && npm install && npm run dev # → http://localhost:4321
```
---
## Status
- [x] Scope and positioning
- [x] Engine choice (Tai-e)
- [x] Gap mapping vs. competitors
- [x] Project scaffold (Maven multi-module, CLI skeleton, config loader, SARIF model)
- [x] Initial benchmark: SQL injection (direct / through-service / via-Kafka / safe), reflected XSS, path traversal, command injection
- [x] Engine: Tai-e IFDS wired end-to-end on the benchmark
- [x] Spring source layer: annotation → Tai-e param-source generation
- [x] Functional CLI with SARIF output
- [x] precision/recall on the current benchmark — **30/30 vulnerable cases detected, 0 false positives** across SQL / JPQL injection (direct / through-service / four-layer / via-Kafka / reactive R2DBC), reflected / conditional / **cross-request stored** XSS, SSRF, SpEL, JNDI, XXE, template injection (SSTI), log injection, path traversal, command injection, open redirect; multi-framework sources (Spring MVC/WebFlux, Kafka, JAX-RS/Quarkus, Micronaut, `@Repository` reads, `@FeignClient`, `@Scheduled`)
- [x] Phase 2 differentiators: `@KafkaListener` source, conditional sanitizers, stored / second-order injection
- [x] GitHub Action (Docker) + self-contained jar + CI workflow
- [x] Web dashboard (React + Vite) for SARIF reports
- [x] Hardcoded-secrets scanner (`secrets` command); mergeable `--config`
- [x] Robustness pass: JNDI / XXE / log / template / JPQL sinks, file-upload and `@MatrixVariable` sources, `Optional` / `CompletableFuture` taint transfers, framework-internal sink filtering
- [x] Configuration & misconfiguration audits: `config` (insecure `application.yml`/`.properties`) and `misconfig` (CSRF/clickjacking disabled, CORS `*`, insecure cookies, sensitive data logged)
- [x] Adoption: per-finding confidence score (console + SARIF), `scan --diff ` for fast pull-request scans, inline `// spring-taint: suppress` comments (`--src` / `suppressions`), and `validate-config` to catch typo'd custom rules
- [x] Advanced sources: `@FeignClient` results (cross-service), `@Scheduled` jobs as entry points, and `@Transactional` write-then-read stored injection
- [x] Near-miss sanitizers (`--src`): flags insufficient (quote-stripping), blacklist, discarded-result, and wrong-context sanitization — the "I'm sure this is safe" class of bug
- [x] Autofix (`--suggest-fixes` / `--fix`): rewrites a concatenated SQL query into a parameterized one; verified end-to-end (applying the fixes drops the benchmark's SQL findings 15 → 1 and the patched code compiles)
---
## Known limitations
Static analysis has inherent limits. For this project:
- **Java reflection** (`Class.forName()`, `Method.invoke()`) can break the flow
- **Spring dynamic proxies** (AOP / CGLib) introduce indirection that may break the call graph
- **Entity / DTO field tracking** — sources are `String`-only (a `@Repository`/`@FeignClient` returning a DTO whose getter is later read is not followed), a deliberate precision-over-recall choice
- **Complex lambdas / method references** — partial coverage via Tai-e
- **The taint analysis runs on JDK 17** — Tai-e 0.5.1 cannot read JDK 21 bytecode
Each release documents its limitations explicitly, alongside the test cases that exercise them.
---
## Contributing
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for the dev
setup, how to add a benchmark case, and the PR checklist. Please also read the
[Code of Conduct](CODE_OF_CONDUCT.md). To report a security issue, see
[SECURITY.md](SECURITY.md).
## Acknowledgements
Built on [Tai-e](https://github.com/pascal-lab/Tai-e) (Nanjing University), which
provides the call-graph construction, pointer analysis, and IFDS taint propagation.
Tai-e is licensed under LGPL-3.0; this project depends on it as a library.
## License
[MIT](LICENSE) © Gabriel Baldez.