To boost the security of our application, we have implemented fuzz testing for our REST endpoints and core components. Fuzzing is a testing technique that provides semi-random data as input to the application to find bugs, crashes, or security vulnerabilities.
We have implemented two types of fuzz tests:
- EndpointFuzzTest.java: A JUnit test class using
@FuzzTestannotations andRestAssuredto fuzz REST endpoints in a running Quarkus test instance. - Purpose: Quick feedback during development and as part of the regular test suite.
- Runs: As part of
mvn test, with configurable duration using-Djazzer.duration=<time>.
These are lightweight, standalone classes that implement the fuzzerTestOneInput method expected by Jazzer's native driver. They run in ClusterFuzzLite's continuous fuzzing pipeline:
- EndpointFuzzer.java: Coverage-guided fuzzing of all REST endpoints by starting Quarkus as a subprocess and sending HTTP requests.
- HashHelperFuzzer.java: Tests the keyed HMAC-SHA-256 hashing functionality with arbitrary string inputs to verify determinism and detect edge cases.
- JsonDeserializationFuzzer.java: Fuzzes JSON deserialization of all request DTOs (ContactgegevenRequest, VoorkeurRequest, DienstverlenerRequest, etc.) to find parsing vulnerabilities.
Key Difference: The JUnit tests run Quarkus in-process with @QuarkusTest, while the ClusterFuzzLite targets are optimized for long-running, coverage-guided fuzzing in CI/CD pipelines.
By default, fuzz tests run as part of the normal test suite but with only a few iterations. To run them for a longer period (which is recommended for finding real issues), you can use the following Maven command:
mvn test -Dtest=EndpointFuzzTest -Djazzer.duration=1m -Djacoco.skip=trueThe -Djazzer.duration=1m flag tells Jazzer to run for 1 minute. You can increase this (e.g., 10m, 1h) for more thorough testing.
Note: We use
-Djacoco.skip=truebecause running only a subset of tests (like just the fuzz tests) will likely fail the JaCoCo coverage check, as the overall project coverage threshold won't be met.
To run only one specific fuzz test method:
mvn test -Dtest=EndpointFuzzTest#fuzzGetPartij -Djazzer.duration=1m -Djacoco.skip=trueTo add a new fuzz test to EndpointFuzzTest:
- Add a method to
EndpointFuzzTest(or create a new test class). - Annotate it with
@FuzzTest. - Use
FuzzedDataProviderto generate semi-random input data. - Use
RestAssuredto send this data to your endpoint.
Example:
@FuzzTest
public void fuzzMyNewEndpoint(FuzzedDataProvider data) {
String input = data.consumeRemainingAsString();
RestAssured.given()
.body(input)
.when()
.post("/api/my-endpoint")
.then()
.extract().response();
}To add a new standalone fuzzer that runs in the CI/CD pipeline:
- Create a new class in
src/test/java/nl/rijksoverheid/moz/fuzzing/. - Implement a
public static void fuzzerTestOneInput(FuzzedDataProvider data)method. - Use
datato consume input and test your functionality. - The fuzzer will automatically be detected by
.clusterfuzzlite/build.shand included in continuous fuzzing.
Example:
package nl.rijksoverheid.moz.fuzzing;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
public class MyComponentFuzzer {
public static void fuzzerTestOneInput(FuzzedDataProvider data) {
String input = data.consumeRemainingAsString();
// Test your component with the fuzzed input
MyComponent.process(input);
}
}Jazzer will automatically stop and report if it finds:
- An unhandled exception that causes the JVM to crash (e.g.,
OutOfMemoryError,StackOverflowError). - Any security-relevant exceptions if configured.
- Assertions that fail.
If Jazzer finds a "finding", it will create a fuzz-test-*.repro file. You can use this file to reproduce the exact input that caused the failure.
To satisfy the GitHub Scorecard "Fuzzing" requirement, this project is configured to use ClusterFuzzLite (CFL).
- ClusterFuzzLite: We have integrated ClusterFuzzLite via GitHub Actions (see
.clusterfuzzlite/and.github/workflows/cflite_*.yml). - Continuous Testing:
- PR Mode: Every pull request triggers a short fuzzing session (5 minutes) to catch regressions before they are merged. This is configured to run on all pull requests with the target branch being
main. - Batch Mode: A longer daily fuzzing session (1 hour) runs on the
mainbranch to discover deeper issues. - Manual Trigger: Fuzzing workflows can also be triggered manually via the GitHub Actions "Run workflow" button.
- PR Mode: Every pull request triggers a short fuzzing session (5 minutes) to catch regressions before they are merged. This is configured to run on all pull requests with the target branch being
- Scorecard Recognition: By having these workflows in place and running them, the OpenSSF Scorecard will recognize that the project is being actively fuzzed, which improves our security score.
.clusterfuzzlite/Dockerfile: Defines the build environment (based onoss-fuzz-base)..clusterfuzzlite/build.sh: Script that compiles the application and prepares the fuzzing targets for Jazzer..github/workflows/cflite_pr.yml: GitHub Action for PR fuzzing..github/workflows/cflite_batch.yml: GitHub Action for scheduled batch fuzzing.