high
CVE
CVE-2026-13502
CWE
CWE-362, CWE-502
Affected Surface
org.antlr:antlr4-maven-plugin 4.13.0 through 4.13.2, Maven builds that execute antlr4:antlr4 and persist build state in target/maven-status/antlr4/dependencies.ser, CI/CD runners with shared workspaces, poisoned caches, or other attacker-controlled writes into the Maven build directory, Developer workstations that build mixed-trust repositories using the ANTLR4 Maven plugin
CVE-2026-13502 landed in NVD on 28 June 2026 as a flaw in org.antlr:antlr4-maven-plugin, the Maven plugin that turns *.g4 grammars into generated parser code during antlr4:antlr4. The NVD text describes the bug as a time-of-check/time-of-use issue in GrammarDependencies.java, but the more important technical detail in the public disclosure is that the plugin deserializes build-state data with ObjectInputStream.readObject() and does not apply an ObjectInputFilter.
That distinction matters for AppSec teams. A bare TOCTOU label can sound like an edge-case stability issue. In practice, the vulnerable path sits in a build plugin, reads attacker-writable state from the build directory, and executes inside developer workstations and CI runners that often hold package-registry credentials, source-control tokens, cloud keys, signing material, and artifact-publishing rights.
Affected package and versions
The affected Maven package is:
org.antlr:antlr4-maven-plugin4.13.0org.antlr:antlr4-maven-plugin4.13.1org.antlr:antlr4-maven-plugin4.13.2
At the time of writing, public references describe the issue and PoC, but do not point to a published upstream fix release yet. Treat every build using those versions as exposed if an attacker can write into the build directory or poison cached build state.
Start with dependency inventory:
rg -n '<artifactId>antlr4-maven-plugin</artifactId>' . --glob 'pom.xml'
Then confirm the resolved version in CI or a checked-out project:
mvn -q help:effective-pom | rg -n 'antlr4-maven-plugin|<version>'
Where the vulnerable state lives
The plugin persists dependency metadata under Maven’s build directory. Antlr4Mojo creates the status file here by default:
@Parameter(defaultValue = "${project.build.directory}/maven-status/antlr4", readonly=true)
private File statusDirectory;
private File getDependenciesStatusFile() {
File statusFile = new File(statusDirectory, "dependencies.ser");
...
return statusFile;
}
In a normal Maven project, that resolves to:
target/maven-status/antlr4/dependencies.ser
That is important because many organizations already make target/, workspace caches, or pre-build artifacts writable to multiple jobs, pre-steps, code generators, or previously compromised dependencies.
The vulnerable code path
The sink is straightforward:
private Map<File, Map.Entry<byte[], Collection<String>>> loadStatus(File statusFile) {
if (statusFile.exists()) {
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(statusFile));
try {
Map<File, Map.Entry<byte[], Collection<String>>> data =
(Map<File, Map.Entry<byte[], Collection<String>>>) in.readObject();
return data;
} finally {
in.close();
}
} catch (Exception ex) {
log.warn("Could not load grammar dependency status information", ex);
}
}
return new HashMap<File, Map.Entry<byte[], Collection<String>>>();
}
Two separate security problems are visible in that snippet:
statusFile.exists()checks the file before it is opened, creating a replaceable race window.- The file contents are deserialized with
ObjectInputStream.readObject()and no class filter.
The public issue is correct to call out the race, but the deserialization sink is the part that turns poisoned build state into a practical code-execution candidate.
Why the deserialization detail matters more than the label
If the status file were just reparsed as text, the race would mostly be about swapping dependency metadata. But Java serialization is different: readObject() is effectively a class-instantiation engine for whatever serializable object graph the stream requests, unless the caller constrains it.
The published write-up demonstrates the core property defenders need to care about: the plugin accepts arbitrary serialized object types and attempts to load attacker-specified classes. In Java 9 and later, ObjectInputFilter exists specifically to narrow this boundary. The ANTLR plugin does not use it here.
That turns the exploit model into:
attacker gains write access to build directory or shared CI cache
-> replaces target/maven-status/antlr4/dependencies.ser
-> developer or CI job runs mvn generate-sources / antlr4:antlr4
-> GrammarDependencies.loadStatus() calls ObjectInputStream.readObject()
-> attacker-controlled object graph loads inside the Maven process
Even if a specific environment lacks a convenient gadget chain, the boundary is still wrong: a build plugin is treating attacker-controlled build-state bytes as trusted Java objects.
Realistic attack paths
The bug is local to the build environment, but that does not make it low-impact. The most realistic paths are all AppSec-relevant:
- A compromised pre-build step writes
dependencies.serinto the workspace before Maven runs. - A malicious dependency or plugin with earlier code execution in the same CI job poisons the ANTLR status file for a later stage.
- A shared self-hosted runner reuses a writable workspace or cache across trust boundaries.
- A developer pulls a repository that already contains a poisoned
target/tree and then runs a normal Maven build.
This is exactly the kind of “not remote, but still supply-chain-relevant” flaw that matters on CI runners. Build systems are high-value because any code execution there can taint artifacts, steal signing keys, or exfiltrate deployment credentials.
Detection and scoping
Find projects that use the plugin:
rg -n '<artifactId>antlr4-maven-plugin</artifactId>' . --glob 'pom.xml'
Find suspicious persisted build state:
rg -n 'dependencies\\.ser' . --glob 'target/**'
Review your CI design for cross-job trust issues:
rg -n 'cache|workspace|artifact' .github .gitlab-ci.yml .circleci .buildkite scripts
Prioritize environments where all of these are true:
antlr4-maven-plugin4.13.0-4.13.2 is present- builds reuse
target/or workspace state - untrusted code can run before the ANTLR goal
- the Maven process holds secrets or can publish artifacts
If you suspect tampering, review CI logs for unexpected failures or warnings around “grammar dependency status information” and preserve the affected dependencies.ser file for forensic analysis before deleting the workspace.
Remediation
There is no published fixed ANTLR release referenced in the public materials yet, so remediation is about reducing the trust boundary immediately:
- Stop reusing untrusted
target/or workspace state between builds. - Delete
target/maven-status/antlr4/dependencies.serbefore the ANTLR goal runs. - Ensure self-hosted runners do not share writable workspaces across repositories or trust levels.
- Treat any build that may have deserialized attacker-controlled state as a code-execution incident.
Practical hardening steps:
rm -f target/maven-status/antlr4/dependencies.ser
mvn clean generate-sources
On CI, prefer fresh workspaces over mutable cache reuse for ANTLR-generated state. If you must preserve build caches, scope them per branch or repository and do not allow attacker-controlled jobs to populate caches consumed by privileged release jobs.
What an upstream fix should look like
There are two distinct fixes the maintainers should make:
First, remove the race and treat file absence as a normal error path instead of doing exists() first:
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(statusFile))) {
...
} catch (FileNotFoundException e) {
...
}
Second, stop trusting raw Java serialization for build-state metadata. The safest option is to replace it with a data-only format such as JSON, CBOR, or another schema-constrained encoding. If serialization remains, apply a strict allowlist with ObjectInputFilter so the stream cannot request arbitrary classes.
Response guidance
If a potentially attacker-controlled dependencies.ser reached a build using an affected plugin version, treat the Maven process as exposed:
- rotate repository and package-publishing tokens available to the job
- invalidate artifacts produced by the affected build
- inspect build logs for unexpected external network activity
- review downstream generated parsers and release artifacts for tampering
The deeper lesson is that build metadata is code-adjacent data. Once a Maven plugin reads it with readObject(), your build cache becomes part of your application security boundary.