# Maven Central

> Maven Central is the default public repository for Java and JVM libraries, serving immutable, signed releases that Maven and Gradle download by coordinates.

Also known as: Central Repository, The Central Repository, Maven Central Repository, repo1.maven.org

Source: https://socket.dev/glossary/maven-central

Last updated: 2026-10-15

Maven Central is the default public repository for Java and other JVM libraries, also called the Central Repository. Publishers prove control of a groupId namespace, then upload immutable releases with POM metadata, PGP signatures, and checksums. Maven, Gradle, and sbt fetch artifacts by coordinates. Its security model rests on namespace ownership, which builds almost never verify.

The mechanism is simple. Every artifact has coordinates, `groupId:artifactId:version`, and the groupId is a reversed domain name or code-host account that the publisher had to prove it controls. Central then guarantees that a published release never changes. Those two properties make Maven Central harder to poison than npm or PyPI in one specific way: an attacker cannot publish `com.fasterxml.jackson.core:jackson-databind:2.99` without controlling fasterxml.com or a Portal account authorized for that namespace. The checks stop there. Maven verifies only SHA-1 and MD5 checksums fetched from the same server as the JAR. In Maven 3.9, a checksum mismatch is a warning, not an error. Maven itself never checks the PGP signatures that Central requires every publisher to upload.

Our position: namespace verification does not stop Java supply chain attacks; it reshapes them. None of the Maven Central incidents we have written up involved a stolen namespace. Two used new namespaces built to resemble real projects, and one arrived through an automated npm-to-Maven mirror. The useful defense is to analyze what is inside the JAR before its coordinates land in a POM, because nothing in the default download path will look.

## What is Maven Central?

Maven Central is a static HTTPS file tree at `repo1.maven.org/maven2`, mirrored at `repo.maven.apache.org/maven2`. Sonatype operates it as a free service. The Apache Maven project defines the repository format and the client. Every Maven installation trusts Central without configuration because the Super POM, compiled into Maven itself, declares it. This excerpt comes from `maven-model-builder` inside Maven 3.9.11:

```xml
<repositories>
  <repository>
    <id>central</id>
    <name>Central Repository</name>
    <url>https://repo.maven.apache.org/maven2</url>
    <layout>default</layout>
    <snapshots>
      <enabled>false</enabled>
    </snapshots>
  </repository>
</repositories>
```

Resolution needs no registry API. Coordinates map to file paths by a fixed rule from the [Maven repository layout](https://maven.apache.org/repositories/layout.html): replace the dots in the groupId with slashes, then append `artifactId/version/artifactId-version.extension`. A client resolves `com.github.scribejava:scribejava-core:8.3.3` by fetching the POM at `com/github/scribejava/scribejava-core/8.3.3/scribejava-core-8.3.3.pom`, reading its dependencies, and recursing. When two paths in the tree ask for different versions of one library, Maven picks the ["nearest definition"](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html) to your project, so a [transitive dependency](https://socket.dev/glossary/transitive-dependency) declared one level up beats a newer one declared three levels down.

Maven Central hosts releases only. The Apache upload guide states the rule plainly: ["The Central Repository will not change or replace a release after it is published."](https://maven.apache.org/repository/guide-central-repository-upload.html) Snapshot builds live in a [separate repository that deletes them after 90 days](https://central.sonatype.org/publish/publish-portal-snapshots/), and snapshots are disabled for `central` in the Super POM above.

Immutability has two consequences. No one, including a hijacked maintainer, can swap the bytes behind an existing version. And a bad release cannot be fixed in place: publishers must ship a new version, while Central's operators remove confirmed malware from the tree entirely. Removal does not reach every copy: MVN Repository, a third-party index, was [still serving the malicious XZ for Java impostor](https://socket.dev/blog/malicious-maven-package-impersonating-xz-for-java-library) after Central removed it in December 2024.

## How do you publish to Maven Central?

You publish to Maven Central by verifying a namespace in the Central Publisher Portal, then uploading a signed bundle that passes automated validation. The legacy OSSRH publishing service [reached end of life on June 30, 2025](https://central.sonatype.org/pages/ossrh-eol/), so older guides that point at `oss.sonatype.org` no longer work.

The namespace step is where Central's security model lives. Central offers [three ways to verify a namespace](https://central.sonatype.org/register/namespace/):

- **Domain namespaces** such as `com.example` require a DNS TXT record on `example.com` containing a verification key.
- **Code-hosting namespaces** such as `io.gitlab.<user>` require a temporary public repository named after the verification key.
- **GitHub sign-up** grants a verified `io.github.<github username>` namespace automatically, with no further proof.

The bundle is a zip laid out in repository format. Every non-POM project needs a main JAR, a `-sources.jar`, and a `-javadoc.jar`. Every file except the checksums needs a `.asc` PGP signature, and every file except the signatures needs `.md5` and `.sha1` checksums. The POM must carry `name`, `description`, `url`, a license, developer information, and SCM URLs.

```text
io/github/example/demo-lib/1.0.0/
  demo-lib-1.0.0.pom          .pom.asc          .pom.md5          .pom.sha1
  demo-lib-1.0.0.jar          .jar.asc          .jar.md5          .jar.sha1
  demo-lib-1.0.0-sources.jar  -sources.jar.asc  -sources.jar.md5  -sources.jar.sha1
  demo-lib-1.0.0-javadoc.jar  -javadoc.jar.asc  -javadoc.jar.md5  -javadoc.jar.sha1
```

Build plugins usually produce and upload the bundle, but the underlying call is one request to the [Portal publisher API](https://central.sonatype.org/publish/publish-portal-api/). The credential is a Portal user token, base64-encoded as `username:password`:

```bash
curl --request POST \
  --header "Authorization: Bearer $(printf '%s:%s' "$TOKEN_USER" "$TOKEN_PASS" | base64)" \
  --form bundle=@central-bundle.zip \
  "https://central.sonatype.com/api/v1/publisher/upload?publishingType=AUTOMATIC"
```

The response is a deployment ID. The deployment moves through `PENDING`, `VALIDATING`, and `VALIDATED`, then `PUBLISHING` and `PUBLISHED`. `USER_MANAGED`, the default, waits for a human to click Publish in the Portal. `AUTOMATIC` publishes as soon as validation passes.

Two dated caveats apply as of October 2026. The Portal's documented credential is a long-lived user token, and Central has announced no OIDC-based trusted publishing of the kind npm and PyPI offer. And since October 1, 2026, Central enforces [monthly per-organization publishing limits](https://central.sonatype.org/publish/maven-central-publishing-limits/), set where the top 10% of publishers by volume begin. The reference 90th-percentile figures are 1,167 files, 78 MB, and 7 releases a month.

## Publish-time checks versus download-time checks

Maven Central verifies namespace control at publish time; Maven verifies almost nothing at download time. The two halves of the system check different things, and the gap between them is where attacks live.

> What Maven Central checks when an artifact is published and what Maven checks when it is downloaded

At publish time, the Portal's security-relevant checks reduce to four: the uploader controls the namespace, the POM carries the required metadata, every file has a PGP signature that validates against a public key on a keyserver, and every file has matching checksums. None of these checks reads the code, so a look-alike namespace such as `io.github.leetcrunch` passes all four. A signature proves that someone holding *a* key signed the file; Central does not check that the key belongs to the project the name suggests. Nothing checks that the `-sources.jar` was compiled into the main JAR.

At download time, Maven 3.9 fetches the JAR and its `.sha1` or `.md5` from the same host. The checksum detects transfer corruption; anyone able to replace the JAR can replace its checksum too. A mismatch only logs a warning, because the default checksum policy is `warn`, a default that the long-standing Maven issue [MNG-5728](https://issues.apache.org/jira/browse/MNG-5728) proposes switching to `fail`. We confirmed the default by instantiating the policy class from the Maven 3.9.11 distribution with a ten-line program:

```text
$ java -cp "/opt/maven/lib/*:." Q
ArtifactRepositoryPolicy() checksumPolicy = warn
ArtifactRepositoryPolicy(true,null,null) checksumPolicy = warn
```

The second line matters because the Super POM declares `central` without a `checksumPolicy`, so the null falls through to `warn`. The `-C` or `--strict-checksums` flag upgrades mismatches to failures. Maven has no flag that verifies signatures. The [resolver configuration reference](https://maven.apache.org/resolver/configuration.html) lists keys for generating GPG and Sigstore signatures and none for verifying them.

The irony is written into Maven's own documentation. The [upload guide](https://maven.apache.org/repository/guide-central-repository-upload.html) says: "If there are no signatures, then users have no guarantee that they are downloading the original artifact." Central collects a signature for every file, and the default client never reads one.

| Check | At publish (Central Portal) | At download (Maven 3.9 default) |
| --- | --- | --- |
| Uploader controls the groupId | Yes: DNS TXT record, code-host repository, or GitHub sign-up | No |
| POM metadata present | Yes: name, description, URL, license, developers, SCM | No |
| PGP signature | Must be present and valid against a keyserver key | Not checked |
| Signing key belongs to the real project | No | No |
| Checksums | .md5 and .sha1 required | SHA-1 and MD5 from the same host; mismatch is a warning |
| Sources JAR matches binary JAR | No | No |
| Name resembles another project | No | No |
| What the code does | No | No |

Central added optional [Sigstore signature validation in January 2025](https://socket.dev/blog/maven-central-adds-sigstore-signature-validation). [Sigstore](https://socket.dev/glossary/sigstore) ties a signature to an identity such as a GitHub account instead of a long-lived PGP key, which is a real improvement for tracing who published. It does not change the download column: consumers still have to verify the bundle themselves, and nothing in Maven does. It does not touch the last row either. A malicious publisher with a fresh GitHub account produces a perfectly valid Sigstore bundle.

## How do attackers get malware onto Maven Central?

Attackers get malware onto Maven Central by acquiring a namespace that looks like a real project's, not by stealing the real one. Two paths common on npm are mostly closed. Namespace ownership blocks claiming an unregistered name that matches an internal package. Plain version strings block auto-upgrades: a POM that declares `1.2.3` gets 1.2.3, and Maven has no caret ranges to pull in a malicious 1.2.4 until someone edits the POM or merges a bot's upgrade pull request. Explicit ranges such as `[1.0,2.0)` exist, but plain versions are the norm. Look-alike namespaces are still cheap.

| Incident | Legitimate coordinates | Malicious coordinates | How the namespace was obtained |
| --- | --- | --- | --- |
| XZ for Java impersonation, published May 12, 2024 | org.tukaani:xz | io.github.xz-java:xz-java | GitHub account named xz-java |
| ScribeJava credential theft, published January 25, 2024 | com.github.scribejava:scribejava-core | io.github.leetcrunch:scribejava-core | GitHub account named leetcrunch |
| Jackson typosquat, December 2025 | com.fasterxml.jackson.core:jackson-databind | org.fasterxml.jackson.core:jackson-databind | Registration of fasterxml.org, which satisfies DNS verification for org.fasterxml |

Each case is [typosquatting](https://socket.dev/glossary/typosquatting) at the namespace level. The artifactId is identical, so a search for `scribejava-core` on Maven Central returned the real library and the copy side by side. The XZ for Java impostor published two benign versions before adding a backdoor in version 1.9 that listened on port 11337 and executed whatever shell script arrived. [The ScribeJava copy](https://socket.dev/blog/malicious-maven-package-exfiltrates-oauth-credentials) shipped six companion artifacts under the same fake groupId to look widely used, and exfiltrated OAuth credentials to Pastebin only on the 15th of each month. [The Jackson typosquat](https://cybersecuritynews.com/hackers-infiltrated-maven-central/) went further: it bought a matching domain in another TLD, then dropped Cobalt Strike beacons on Linux and macOS, according to the reporting.

The ScribeJava case exploits a quirk of Central's history. Central [stopped accepting new `com.github` namespaces on April 1, 2021](https://central.sonatype.org/changelog/), so new GitHub-based projects can only get `io.github.<user>`. Older projects such as ScribeJava still live under `com.github`. An `io.github` copy of a `com.github` library therefore looks like a plausible migration, not an obvious fake.

Two more paths lead around namespace verification:

- **Expired ownership.** In January 2024, Oversecured's [MavenGate research](https://oversecured.com/blog/introducing-mavengate-a-supply-chain-attack-method-for-java-and-android-applications) found 3,710 of 26,163 domains behind Maven Central groupIds available to buy or at auction. Whoever buys a lapsed domain can pass the DNS TXT check for its groupId and publish new versions under it. Oversecured reports that Central then blocked the vulnerable groupIds it found. The structural issue remains: control of the domain is the whole proof.
- **Automated mirroring.** The `org.mvnpm` namespace republishes npm packages as Maven artifacts so Java web frameworks can consume them. During [the second Shai-Hulud wave](https://socket.dev/blog/shai-hulud-strikes-again-v2), that pipeline rebuilt a compromised PostHog release as `org.mvnpm:posthog-node:4.18.1`, carrying the same Bun-based payload the [npm worm](https://socket.dev/glossary/shai-hulud) used. Central purged it the same day we reported it. A namespace check verifies the mirror operator, not the upstream code it ingests.

Classic [dependency confusion](https://socket.dev/glossary/dependency-confusion) is harder here than on npm or PyPI, because an attacker cannot claim your company's `com.yourcompany` groupId without your DNS. The risk reappears inside corporate [artifact repositories](https://socket.dev/glossary/artifact-repository) that merge Central with internal repositories, and in internal groupIds built on domains the company never registered, which anyone can buy.

## Where does a Maven dependency's code run?

A Maven dependency's code can run during the build, at compile time, during tests, at application startup, and when called, but downloading it executes nothing. Maven has no equivalent of npm's [install scripts](https://socket.dev/glossary/postinstall-scripts), so a JAR sitting in `~/.m2/repository` is inert. That is a real advantage, and it is narrower than it sounds.

> Six stages of a Maven build and whether a dependency's code can run in each

| Stage | Does dependency code run? | Example |
| --- | --- | --- |
| Resolve | No. POMs and JARs are copied into ~/.m2/repository. | None: Maven has no install scripts |
| Build plugins | Yes. Plugins are artifacts downloaded from Maven Central by default and run with the build's permissions. | Any plugin in the POM |
| Compile | Yes on JDK 22 and earlier, without configuration. Annotation processors on the classpath run inside javac. | Our 1,872-byte probe below |
| Test | Yes. Test-scoped dependencies load into the test JVM on every CI run. | Test frameworks and mocks |
| Startup | Yes, without your code calling the dependency. ServiceLoader and Spring Boot auto-configuration instantiate classes named in JAR metadata. | Jackson typosquat's Spring @Configuration loader |
| Call | Yes, when your application uses the API. | ScribeJava copy: theft on the 15th of each month. XZ for Java copy: shell on port 11337 |

Compile time is the least appreciated of these. `javac` discovers annotation processors through `META-INF/services/javax.annotation.processing.Processor` in any JAR on the classpath and runs them inside the compiler. Maven passes your dependencies to `javac` as the classpath. We built a 1,872-byte JAR containing one processor and compiled an empty class against it with JDK 21.0.12:

```java
@SupportedAnnotationTypes("*")
public class BuildTimeProbe extends AbstractProcessor {
  @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);
    // Runs inside javac. Any code here executes on the build machine.
    System.err.println("[probe] running inside javac as " + System.getProperty("user.name")
        + ", env vars visible: " + System.getenv().size());
  }
  @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }
  @Override public boolean process(Set<? extends TypeElement> a, RoundEnvironment r) { return false; }
}
```

```text
$ javac -d . BuildTimeProbe.java
$ echo com.example.BuildTimeProbe > META-INF/services/javax.annotation.processing.Processor
$ jar cf innocent-util-1.0.jar META-INF/services com
$ javac -cp innocent-util-1.0.jar -d out App.java
Note: Annotation processing is enabled because one or more processors were found
  on the class path. A future release of javac may disable annotation processing
  unless at least one processor is specified by name (-processor), or a search
  path is specified (--processor-path, --processor-module-path), or annotation
  processing is enabled explicitly (-proc:only, -proc:full).
  Use -Xlint:-options to suppress this message.
  Use -proc:none to disable annotation processing.
[probe] running inside javac as root, env vars visible: 182
$ javac -proc:none -cp innocent-util-1.0.jar -d out App.java
$
```

The processor ran with the build's full environment, which in CI often means deploy tokens and cloud credentials. With `-proc:none` it did not run. [JDK 23 made `-proc:none` the default](https://inside.java/2024/06/18/quality-heads-up/) when no processor is configured explicitly, so builds on JDK 23 or later stop running classpath processors implicitly. Builds on JDK 22 and earlier still run them, with only the note above as a warning.

Startup is the other path that needs no call from your code. Java's `ServiceLoader` and Spring Boot auto-configuration instantiate classes named in a JAR's metadata files. The Jackson typosquat used Spring `@Configuration` to run its loader when the application started. Nothing in the dependency tree had to be invoked by name.

## Defenses that pin bytes, and defenses that read code

The defenses that work for Maven Central dependencies either pin the exact bytes you already reviewed or inspect new artifacts before they enter the build. Most teams run neither.

| Control | What it verifies | Catches | Misses |
| --- | --- | --- | --- |
| Maven default | SHA-1/MD5 from the same host, warn only | Corrupted downloads, sometimes | Any malicious artifact, any tampered mirror |
| mvn -C | Same checksums, fail on mismatch | Corrupted downloads | Anything served with a matching checksum |
| Maven trusted checksums | SHA-512 of every resolved artifact, recorded once and committed | Any change to a byte you already accepted | A dependency that was malicious when you recorded it |
| Gradle dependency verification | Recorded checksums, optionally PGP keys you trust | Changed bytes; a new signing key on a known artifact | New coordinates you add yourself |
| PGP verification against a pinned key per groupId | Signature against a key you chose for each groupId | A hijacked namespace signing with a new key | A legitimate maintainer key used to sign malware |
| Pre-install behavioral analysis | What the JAR, sources JAR, and POM actually contain and do | Look-alike namespaces, backdoors, credential theft | Nothing is complete; dormant logic can still hide |

Trusted checksums are Maven's closest equivalent to a [lockfile](https://socket.dev/glossary/lockfile). Enable them in `.mvn/maven.config` and record once in a clean environment:

```text
--strict-checksums
-Daether.checksums.algorithms=SHA-512,SHA-1,MD5
-Daether.trustedChecksumsSource.summaryFile=true
-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums/
-Daether.artifactResolver.postProcessor.trustedChecksums=true
-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-512
-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=true
```

Verdict: turn on trusted checksums in Maven or dependency verification in Gradle, because they cost one commit and make every later byte change fail the build. Then accept what they cannot do. Every row above the last one assumes the artifact was safe the first time you saw it, and none of the three incidents above changed bytes after publication. They were malicious from the first download. Only analysis of new artifacts addresses that case.

That is where we work. Socket analyzes every file in a Maven package, including the compiled JAR and the `-sources.jar`, which is how [we flagged the XZ for Java backdoor](https://socket.dev/blog/malicious-maven-package-impersonating-xz-for-java-library): its source comments described the backdoor, while developers who only consume binary JARs would never see them. [Socket Firewall Enterprise](https://socket.dev/blog/socket-firewall-enterprise) proxies Maven traffic and blocks packages we have flagged before they reach `~/.m2`; the free tier covers only JavaScript, Python, and Rust. Neither replaces pinning. They cover the first download, which pinning cannot.

The deeper open problem is the gap between the sources JAR and the binary JAR. Maven Central requires both, and nothing proves one was built from the other. Rebuilding releases from source and comparing bytes is the only real answer, and projects such as [Reproducible Central](https://github.com/jvm-repo-rebuild/reproducible-central) do it for a small fraction of artifacts. Until a rebuild is the norm, the code in a Maven Central JAR is whatever its publisher compiled, and the only way to know what it does is to look.
