When the Supply Chain Fights Back

Most Java shops treat Maven repositories as read-only infrastructure: they pull well-known artifacts from trusted sources and never think twice about what happens inside the proxy server sitting between their developers and the public internet. That trust is exactly what security researchers like GHSL’s Pascal Chevalier have been poking at.

Chevalier’s recent work focuses on a particularly nasty class of attacks: a malicious actor publishes a hand-crafted JAR to Maven Central (which allows anyone to publish, subject to groupId ownership), then waits for an in-house repository manager to proxy that artifact. Once fetched, these managers don’t just store the bytes; they unpack archives, parse embedded pom.xml descriptors, build dependency graphs, run malware scanners, and display content in admin UIs. That processing step turns a simple dependency download into a second-order attack surface that is notoriously hard to secure through automated testing.

The audit covered the two dominant repository manager products—Sonatype Nexus and JFrog Artifactory—plus the open-source Reposilite project. The result: four CVEs across two products, one of which allows password theft by a single malicious artifact.

The Proxied Artifact as a Weapon

A proxy repository is typically reachable by anonymous users for downloads. This is by design, but it also means any attacker can upload a malicious artifact to public Maven Central and then convince a target repository manager to fetch it—simply by making a request for that artifact through the proxy. The trick is that the malicious payload only needs to be proxied, not hosted on the internal network.

Once the artifact is stored, the manager’s admin UI will routinely display its metadata, including parsed pom.xml content. Since repository managers usually serve both the admin interface and artifact content from the same origin (same protocol, host, and port), any HTML that includes JavaScript embedded in the artifact is executed in the security context of the admin session.

A Basic But Devastating Stored XSS

This is not a clever bug.

An artifact JAR or pom.xml containing a snippet like the following—identified here as a simple HTML file inside the artifact—gets rendered unescaped in the repository manager’s UI when an admin browses to the artifact details:

<?xml version="1.0" encoding="UTF-8"?>
<a:script xmlns:a="http://www.w3.org/1999/xhtml">
    alert(`Secret key: ${localStorage.getItem('token-secret')}`)
</a:script>

For Reposilite (tracked as CVE-2024-36115), the exploit goes straight to the victim’s browser localStorage, where the user’s token-secret is stored. With that value in hand, the attacker can impersonate the admin from any other device.

In Sonatype Nexus 2 (CVE-2024-5083), the same class of flaw allows a malicious artifact to drive authenticated requests against the admin API, potentially replacing other stored artifacts. The final link in the attack chain is the developers who then download the poisoned artifact and run its code locally—the classic “key to the kingdom” scenario.

Simple, high-impact fixes exist for both flaws. The first is serving artifact content with a Content-Security-Policy: sandbox header, which forces the browser to treat the resource as cross-origin regardless of the URL. The second is sending Content-Disposition: attachment for raw artifact files, preventing the browser from rendering them as HTML at all.

Escaping HTML is not an option here; it would break the legitimate functionality that shows the user what a pom.xml contains. A sandboxed iframe would be another option but carries its own pitfalls.

Path Traversal Returns in Javadoc Handling

The repository managers under test all rely on Java’s ZipInputStream to process archives in-memory, which protects against the classic archive-extraction-to-disk path traversal. Chevalier found the exception in Reposilite’s support for JavaDoc archives—a subsystem designed to ingest third-party documentation and serve it to users.

Reposilite’s JavadocContainerService.kt extracts individual files from the archive and writes them to a local directory based on the file name directly from the archive:

jarFile.entries().asSequence().forEach { file ->
    if (file.isDirectory) {
        return@forEach
    }

    val path = Paths.get(javadocUnpackPath.toString() + "/" + file.name)

    path.parent?.also { parent -> Files.createDirectories(parent) }
    jarFile.getInputStream(file).copyToAndClose(path.outputStream())
}.asSuccess<Unit, ErrorResponse>()

That line, using file.name as a component of the destination path, is vulnerable when the archive entry name contains traversal characters such as ../../. Because the file does not check for .. before constructing the output path, a crafted JavaDoc archive from an upstream repository can overwrite any file on the server that the repository manager can write to (CVE-2024-36116). Reposilite loads plugins from a designated directory, so a malicious artifact can drop a .jar there and leverage that to achieve code execution.

The companion flaw resides in the route that serves files from the extracted JavaDoc tree. When a user requests a resource via the GET /javadoc/{repository}/{gav}/raw/<resource> endpoint, the URL-encoded resource parameter can include /../ path traversal sequences. Reconstructed naively alongside the base directory, the path escapes the intended JavaDoc folder and allows a remote user to request files from anywhere the process has filesystem read access to (CVE-2024-36117). In the proof-of-concept, that meant reading the internal database, which contains user-account credentials and, in some installations, administrative secrets.

Reposilite file read

The fixes have been released upstream, but any Reposilite instance exposed to public Maven repositories or other unprivileged users is vulnerable until upgraded. When you also consider that both XSS and traversal bugs can be triggered simply by requesting a publicly available artifact through the proxy, this class of vulnerability deserves more attention in internal infrastructure—automated scanners rarely reach the code paths that expand untrusted archives into a local UI, but those paths are exactly the ones an attacker will target.

When Trusted URLs Lie: Name Confusion in Proxy Repositories

Repository managers that cache artifacts from remote sources must translate incoming URL paths into Maven's GroupId, ArtifactId, and Version (GAV) coordinates. The officially documented layout suggests this mapping:

/${groupId}/${artifactId}/${baseVersion}/${artifactId}-${version}-${classifier}.${extension}

GroupId may contain multiple path segments, translated to dots during parsing. A request like GET /org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.tar.gz maps cleanly to the expected coordinates:

groupId: org.apache.maven
artifactId:apache-maven
version: 3.8.4:bin:tar.gz
classifier: bin
Extension: tar.gz

That mapping only works if parsing behaves exactly like a regex match. In practice, URL decoding, path normalization and control characters create room for misinterpretation.

Encoded special characters get decoded and treated as part of the artifact name. A path containing %23 (a literal #) is a case in point:

GET /com/company/artifact/1.0/artifact-1.0.jar%23/xyz/anything.any?isRemote=true

The proxy sees the full decoded path and forwards it upstream, but on the upstream server everything after the hash is stripped off as fragment data. The path gets truncated, effectively letting an attacker plant files with arbitrary names and extensions as long as the path starts with a known prefix — without any write permission to the proxy:

Name confusion arbitrary extension

This quirk affects nearly every product tested, but it's barely exploitable on its own since no client requests artifacts with such names.

Path Traversal via Semicolon

JFrog Artifactory handles the semicolon character specially: everything after ; is treated as "path parameters", not part of the artifact name. Yet the full URL still gets forwarded upstream:

GET /com/company1/artifact1/1.0/artifact1-1.0.jar;/../../../../company2/artifact2/2.0/artifact2-2.0.jar

Artifactory keeps its path parsing at artifact1-1.0.jar, but sends the complete URL upstream. Nexus 3 and some public servers, by contrast, normalize the path to /company2/artifact2/2.0/artifact2-2.0.jar, matching RFC 3986 behaviour.

When Artifactory proxies an external repository, the discrepancy becomes a real vulnerability: artifact poisoning (CVE-2024-6915). An attacker can save any HTTP response from the remote endpoint as an arbitrary artifact on the Artifactory instance. The trivial attack is to publish a malicious artifact upstream — possibly under a test name — then save it as something common, such as spring-boot-starter-web. Subsequent client downloads fetch attacker-controlled content.

Even without upstream write access, the bypass works via an open redirect or reflected XSS on the upstream server: Artifactory doesn't validate what follows ;/../, allowing any relative URL path. The main requirement is that the upstream server performs path normalization on /../. Maven Central doesn't, but public repositories including Apache and JitPack do. And the flaw isn't Maven-specific — it applies to any proxy type Artifactory supports, including npm and Docker repositories.

artifact/../ artifact/%2e%2e/
Maven Central repo1.maven.org
Apache repository.apache.org ✓*
JitPack jitpack.io
npm
Docker Registry
Rubygems.io
Python Package Index (PyPI)
GO package registry (gocenter.io)
  • “✓” means path traversal is accepted by repository, “✗” – not

CVE-2024-6915 in Practice

The demonstration used an Artifactory instance proxying to npm. The layout differs, but the principle holds: overwrite one package's package.json with another's content. In this case, replacing the manifest of is-even with that of is-odd:

Is even attack Is even poisoned

The npm client warns when the installed package's name (is-odd) mismatches the requested name (is-even), but as long as the downloaded file is valid JSON and contains links to the source archive, the client will proceed to download and execute it. npm clients are built on the premise that the registry source is trustworthy. Once that premise breaks — as it did here — checksums won't help either, since an attacker who can overwrite the file can overwrite its hash too.

npm confused

The report earned a critical-severity designation and a $5,000 bug bounty from JFrog, donated (as part of the work at GitHub) to Cancer Research UK.

Query Parameters as Exploitation Aids

Nexus and JFrog support special URL query parameters for proxy repositories. Artifactory accepts these:

magic parameters jfrog

Nexus 2 offers fewer interesting ones, but these two stand out for attackers:

magic parameters nexus

Such parameters can be applied on the proxy side or smuggled upstream via URL encoding, altering how requests are interpreted in ways an attacker can turn to their advantage. For example, the :properties suffix triggers a local redirect. Artifactory normally skips path normalization on incoming requests, but the redirect forces the client to normalize, enabling a path traversal for name confusion attacks.

Nexus 2: From Metadata Writes to Pre-Auth RCE (CVE-2024-5082)

Repository managers persist more than user uploads. Alongside artifacts sit checksums, timestamps, uploader identity and other metadata stored in the same directory tree. Nexus 2 is an extreme case, keeping files like:

  • /.meta/repository-metadata.xml — repository properties in XML
  • /.meta/prefixes.txt
  • /.index/nexus-maven-repository-index.properties
  • /.index/nexus-maven-repository-index.gz
  • /.nexus/tmp/<artifact>nx-tmp<random>.nx-upload — temporary upload file
  • /.nexus/attributes/<artifact-name> — per-artifact JSON metadata file

Only the last of these is actually protected. Nexus rejects direct uploads or downloads under /.nexus/attributes/:

nexus attributes forbidden

The filter is bypassable by switching the URL prefix to the local repository API path (/nexus/service/local/repositories/test/content/) and inserting a double slash before .nexus/attributes:

nexus attributes bypass

Reading those local attribute files has limited value. Overwriting them via PUT is more interesting. Release repositories forbid artifact content updates by default, but attributes for maven-metadata.xml files can be replaced:

nexus velocity content generator

That's where "contentGenerator":"velocity" enters the picture. Any artifact whose attributes carry this key gets its content rendered as a Velocity template. Upload a maven-metadata.xml with a template payload, update its attributes to enable the generator, and the template executes on the next fetch:

nexus put shell nexus exec shell id

The proof-of-concept template invokes java.lang.Runtime.getRuntime().exec("id").

Getting There Without Credentials

The PUT requests above require an account with upload rights on the target Nexus, a serious constraint in practice. The author pursued both available workarounds.

First, pairing this bug with a previously discovered stored XSS (CVE-2024-5083) that exists in Nexus proxy repositories could chain a write-free payload — but exploiting the XSS still demands an admin viewing the malicious artifact with a valid session.

Second, and more interesting: trigger the metadata overwrite through a proxy repository. Publishing an artifact under the Maven Group ID .nexus/attributes upstream is unrealistic on well-governed repositories, but the path traversal trick from name confusion attacks eliminates that obstacle. An artifact with a harmless coordinate like org.example can be forced to land in /.nexus/attributes/…:

GET /nexus/service/local/repositories/apache-snapshots/content//.nexus/attributes/%252e./%252e./com/sbt/ignite/ignite-bom/maven-metadata.xml
nexus apache snapshots trick

Nexus decodes the URL, yielding /.nexus/attributes/%2e./%2e./com/sbt/ignite/ignite-bom/maven-metadata.xml, then forwards that to Apache Snapshots. The upstream server normalizes the path and serves the requested file content back, which Nexus stores under /.nexus/attributes/. Apache Snapshots is enabled by default in Nexus installations, and fetching from it is an anonymous GET.

The realistic attack chain: an attacker publishes their own artifact to Apache Snapshots and uses it to hit every exposed Nexus instance that proxies it. Enumerating Apache committer names is trivial — the committer index is public — and credentials may leak from password dumps. The author explicitly declines to test that scenario as out of legal and ethical scope.

Conclusions

Running repository managers like Nexus, JFrog Artifactory or Reposilite in proxy mode expands the attack surface reserved for unauthorised actors. Every tested solution parses, indexes and stores artifacts, and a carefully crafted artifact becomes a weapon against the very manager that processes it: XSS, XXE, archive expansion and path traversal all surfaced.

URL decoding differences let #, ; and encoded characters trigger parsing discrepancies between what a proxy believes it stores and what the upstream repository actually serves. Combined with local artifact caching, these issues turn into cache poisoning (CVE-2024-6915 being the JFrog showcase).

The ecosystem's central repositories run on a handful of partially open source tools. Their vendors run strong security teams and bounty programmes, yet critical flaws remain. Finally, these attacks are in no way Maven-specific — npm, Docker, RubyGems and every other ecosystem that leans on proxy caching inherits the same mechanics. Testing the proxy functionality of other products is likely to yield further findings.

The research was presented at the Ekoparty Security Conference in November 2024.