
By Javier Medina ( X / LinkedIn)
TL;DR
Relax. This isn’t the umpteenth we invented repojacking post.
This is a measurement and validation write-up about a specific and non-trivial to exploit repojacking pattern where the registry is a build service with state (JitPack). What makes it worth writing is how delivery can happen through JitPack’s rebuild surface and through the historical gaps that never became sealed artifacts.
In one sentence:
Can immutable artifacts stay trustworthy when their coordinates still depend on a name that can change hands?
We’ll show:
- The failure mode and why namespace changes matter in JitPack’s model
- The auth and permission hinge and how a reborn namespace can sometimes manage JitPack build states for versions that were never sealed as artifacts
- A short lab you can reproduce safely using namespaces you control
- The tools we built and are releasing to measure this at scale
- Real targets we found in the legacy Android ecosystem and how risk depends on JitPack build state and Git platform protections.
This work also serves, although not originally intended, as an operational validation of the countermeasures against repojacking deployed by GitHub and Bitbucket.
Disclosure Status: This research follows a coordinated disclosure schedule that began in January 2026. At the time of publication, neither JitPack nor GitHub has responded to any of our attempts to contact them. The behaviors described below remain active and unmitigated on their part.
0# An honest view on this topic
Every research lab has the same guilty dream: find a clean supply chain weakness, name it and ship a write-up people remember for years. We wanted our own repojacking story too so we went looking in the Android long tail. We did it with more heart than head, and more passion than experience. That’s how we are.

Looking back with the benefit of hindsight, we can now say that this is not the best repojacking story ever told, but it is the one we can tell.
What we did find was a chain that we haven’t seen documented before, based on reborn namespaces and open build states from JitPack. We also confirmed the preconditions exist in real projects, not only in a lab.
It’s a story with limitations and it still has practical lessons. That is why we are writing it down.
1# Reborn namespaces in Git-backed coordinates
JitPack turns a Git repository into a Maven-style dependency.
A typical setup looks like this:
repositories { maven { url "https://jitpack.io" }}dependencies { implementation "com.github.User:Repo:Tag"}
JitPack supports multiple Git hosts. The main difference is the groupId prefix. Bitbucket uses org.bitbucket.*, GitLab uses com.gitlab.*, Github uses com.github.* and so on.
Under the hood, JitPack resolves the host, clones the repo, builds it and serves the artifact.
The first weak point is simple and well known. The dependency coordinate includes a Git namespace. Namespaces are human-chosen names so they can be renamed, deleted, reclaimed or change ownership over time.
So the dependency line can stay exactly the same while the namespace behind it changes over time.
Repojacking quick notes
This is classic repojacking. A stable dependency string can end up pointing somewhere else because a name in the path changed meaning over time. This is a widely discussed topic, and one on which GitHub, for example, has released partial mitigations.
What matters here is what JitPack does with that change.
2# Getting Spicy: Breaking JitPack immutability

This is where things start to get interesting.
Git gives you a name and JitPack gives that name a build pipeline.
JitPack does not behave like a classic package registry such as Maven Central. It builds from Git and it keeps build state. For a given coordinate, you typically see two situations.
Sealed and frozen
The version built successfully, an artifact exists and after a short window (JitPack states seven days for public artifacts) the artifact is treated as immutable. Under normal use it is not expected to be rebuilt and our checks matched that.
Still open
The coordinate can trigger new builds.
Common cases:
- Snapshots such as
main-SNAPSHOT - Dynamic selectors such as
1.+that can move as metadata changes - Failed builds that never produced an artifact
- Recent tags still within the deletion window
- Versions referenced by others but never built
That leads to a simple idea. If a coordinate is still open then a rebuild will use whatever repo the coordinate resolves to at that moment.
Identity matters
Here’s the part that gets overlooked.
JitPack can build from many Git hosts. But when we log in to JitPack, the account model is largely GitHub-centered. GitHub is the main identity. For other hosts there is nothing like SSO to GitLab or Bitbucket. In practice, integrating non-GitHub hosts typically means adding a token in JitPack.
This matters because JitPack allows build deletion in limited cases and it requires authentication. JitPack’s deletion rules are simple:
- A delete endpoint exists
- Authentication is required
- You must have push permissions on the repository
- Deletion is limited to failed builds, snapshots and tags newer than seven days
Now we should combine that with a reborn namespace.
If a namespace is reclaimed and the new owner can satisfy that permission check then the new owner can sometimes manage the parts of JitPack state that are still allowed to change. To be clear, this does not mean rewriting frozen artifacts. The point is the gap cases where nothing was ever sealed and the coordinate is still open.
That is the awkward part: immutability exists, but it is conditional. And there, as we will demonstrate later, there is a risk. Perhaps small, but no less true for that.
3# Case Study: A lab is worth a thousand words
This lab shows the full chain in a controlled setup. The goal is simple: prove that the same JitPack coordinate can end up producing different artifacts after a namespace event, and explain why “failed build gaps” plus Git OAuth matters.
Everything here is done with accounts and namespaces we control. We simulate how an attacker can abuse GitHub redirects (301) to hijack a library used via JitPack.
Target Coordinate: com.github.this-is-a-lab-repo-for-jitpack:LAB1
The Setup
We pre-created a repository with three specific states to demonstrate different vulnerabilities.
- Version 1.1: A successful build. JitPack has “frozen” this version. It’s safe.
- Version 1.0: A failed build. No artifacts were produced.
- main-SNAPSHOT: A dynamic version pointing to the latest code.

The Baseline
Before the attack, we verify the normal behavior.
- Fetching 1.0: The build fails because the artifacts are missing.
$ gradle clean build --refresh-dependencies || true(...)* What went wrong:Could not resolve all files for configuration ':compileClasspath'.> Could not find com.github.this-is-a-lab-repo-for-jitpack:LAB1:1.0. Searched in the following locations: https://repo.maven.apache.org/maven2/com/github/this-is-a-lab-repo-for-jitpack/LAB1/1.0/LAB1-1.0.pom https://repo.maven.apache.org/maven2/com/github/this-is-a-lab-repo-for-jitpack/LAB1/1.0/LAB1-1.0.jar https://jitpack.io/com/github/this-is-a-lab-repo-for-jitpack/LAB1/1.0/LAB1-1.0.pom https://jitpack.io/com/github/this-is-a-lab-repo-for-jitpack/LAB1/1.0/LAB1-1.0.jar...BUILD FAILED
- Fetching 1.1: The build succeeds and prints “A_1_1”.
$ gradle clean runApp --refresh-dependenciesDownload https://jitpack.io/com/github/this-is-a-lab-repo-for-jitpack/LAB1/1.1/LAB1-1.1.pomDownload https://jitpack.io/com/github/this-is-a-lab-repo-for-jitpack/LAB1/1.1/LAB1-1.1.jar> Task :runApp A_1_1BUILD SUCCESSFUL
The Hijack Event
This is a classic repojacking event.
- We renamed original GitHub account to
this-is-a-lab-repo-for-jitpack-new

- GitHub created a 301 Redirect.
git push origin main…remote: This repository moved. Please use the new location:remote: git@github.com:this-is-a-lab-repo-for-jitpack-new/LAB1.gitTo labpoc:this-is-a-lab-repo-for-jitpack/LAB1.gitb22bec3..1503b32 main -> main
- We immediately registered the old username
this-is-a-lab-repo-for-jitpackand recreated the repo with some changes. After this, the 301 Redirect isn’t working.

Exploiting SNAPSHOT TAG
Although it may seem obvious, it was necessary to demonstrate that the SNAPSHOT tag is inherently mutable.
We push the initial marker to the repository (“this is main”). The build resolves the snapshot to commit hash c95132fb76.
$ gradle clean runApp --refresh-dependenciesDownload .../LAB1-main-c95132fb76-1.jar> Task :runAppthis is main
We modify the code on the same branch to push a new mark (“this isn’t main”). Forcing a refresh downloads a new artifact (b22bec3951) for the same coordinate.
$ gradle clean runApp --refresh-dependenciesDownload .../LAB1-main-b22bec3951-1.jar> Task :runAppthis isn't main
This means SNAPSHOT and dynamic selectors (like 1.+) are the easiest place for repojacking to matter. If the upstream repo is taken over and a rebuild happens, the resulting JitPack artifact will track that takeover.
Exploiting Failed Builds
Now the more interesting case.
Version 1.0 originally failed to build. Because no artifact exists, it never became a frozen release. It is an open slot.
JitPack allows deleting builds in limited cases and it requires authentication with push permissions on the repository. In our lab, after the namespace event, we sign in to JitPack using Git OAuth as the new owner of the reborn namespace and delete the failed build record for 1.0.
$ curl -u"[REDACTED]:" -X DELETE "https://jitpack.io/api/builds/com.github.this-is-a-lab-repo-for-jitpack/LAB1/1.0"< HTTP/2 200...{ "status" : "ok", "message" : "Starting delete"}
With the failed state cleared, we push buildable code under tag 1.0 in the reborn repo. The next resolution attempt triggers a new build and now the same coordinate becomes a real artifact.
$ gradle clean runApp --refresh-dependenciesDownload https://jitpack.io/com/github/this-is-a-lab-repo-for-jitpack/LAB1/1.0/LAB1-1.0.pomDownload https://jitpack.io/com/github/this-is-a-lab-repo-for-jitpack/LAB1/1.0/LAB1-1.0.jar> Task :runAppthis is bad!BUILD SUCCESSFUL
This is the core point of the lab. We are not overwriting a frozen artifact. We are turning a missing version into a real artifact under a coordinate that already exists in build files.
Lab Highlights
We proved three things with a controlled setup:
- A JitPack coordinate can stay unchanged while the repository behind it changes after a namespace event.
- Snapshots and dynamic selectors are rebuild friendly by design. When a rebuild happens they follow whatever the coordinate resolves to at that time.
- Failed build gaps and “never built” coordinates matter. A version that did not exist as an artifact can later become a real artifact under the same coordinate if the identity behind the coordinate changes and the build state is still open.
What this lab does not prove is “you can overwrite anything”. We did not find a reliable way to replace a public artifact that was already built and frozen.
With that out of the way, the next question is the only one that makes sense to ask.
Does this really matter, or is it just geeky stuff to talk about at a security conference and make yourself look cool?
To answer it, the only thing we could think of was to measure how often do these open states show up in real projects and which coordinates are still referenced today.
4# the Android long tail

We focused on the brittle end of Android builds: older projects, older Gradle patterns.
We scanned 500 public build.gradle files from Github, dated on projects from 2015 to 2019, that used jitpack.io and legacy compile lines. This is an intentionally biased sample to find abandoned coordinates. After all, we are talking about necromancy.
We validated it in December 2025. Roughly 4% (20 projects) referenced at least one coordinate whose upstream Git namespace was not stable at measurement time.
The findings fell into two buckets.
Redirect live (301)
These are the cases where the old namespace still resolves through a redirect and the JitPack project page is alive, so build state and demand are observable.
com.github.lzyzsd:jsbridge: WebView JS bridge (often payment-adjacent flows). Redirect live. Version 1.0.4 appears as a failed build, although it continues to receive around 8,500 requests in the last month.

com.github.apl-devs:appintro: App onboarding flow. Redirect live. Version 4.2.2 shows as failed and still gets roughly 377 requests in the last month, around 25% of the repo activity. Its development is clearly still active, and there are new beta versions released on the apl-devs user’s jitpack coordinates.

These figures may not be enough to make us jump for joy, but they do validate the theory. There are repositories with potentially claimable names that have build errors and therefore do not present immutability in artefacts that continue to be consumed.
Digital void (404)
We also found coordinates pointing to namespaces that return 404 today and do not have a usable JitPack project at the time of validation:
com.github.jkcclemens:khttp: Kotlin HTTP lib.com.github.asbyth:ForgeGradle: Build toolchain / Gradle plugin.org.bitbucket.gruveo:gruveo-sdk-android: Multimedia SDK used via wrappers.
These cases still matter as fragility. The coordinate remains in build files long after the upstream identity is gone. But because current artifact state is not observable, we treat them as “void” cases rather than confirmed delivery risk today.
Top 5 Summary
We have provided a summary of the number of references found in Gradle files for the five most significant projects that we have identified as being at risk. It’ s important to note that there are public projects that use mutable tags.
| Target | Dynamic or snapshot | Fixed tag | Commit pin |
|---|---|---|---|
com.github.lzyzsd:jsbridge | 0 | 126 | 0 |
com.github.apl-devs:appintro | 0 | 202 | 0 |
com.github.jkcclemens:khttp | 12 | 45 | 4 |
com.github.asbyth:ForgeGradle | 7 | 0 | 36 |
org.bitbucket.gruveo:gruveo-sdk-android | 0 | 3 | 0 |
5# Defensive Takeover: Going Beyond Theory
At this point, given the active nature of these risks and the absence of a response from the platforms, we decided to move from passive scanning to active mitigation.
We executed a defensive takeover on the most critical targets identified.
This served a dual purpose.
First, to secure the ecosystem prior disclosure. In the absence of a vendor response or collaboration, taking over the namespace was the only effective way to prevent potential exploitation after disclosure of this research.
Secondly, to validate the actual risk operationally beyond the laboratory. We needed to understand if current anti-repojacking mechanisms (like namespace retirement or blocked usernames) stop this specific vector and in what way.
The results of this phase revealed that these protections are neither uniform nor predictable.
Full Takeover: AppIntro and khttp
The most significant finding was com.github.apl-devs:appintro.
This is a widely used library for on-boarding flows, referenced in hundreds of projects. The original username was moved, leaving a massive void. Despite its continued usage and active development, GitHub’s namespace retirement protection did not trigger.
- Outcome: We were able to register the
apl-devsusername and recreate theAppIntrorepository. - JitPack State: We successfully took control of the JitPack project page.
- Mitigation: We have deployed a security placeholder. Any build still pointing to the old coordinate will now pull a safe, non-functional artifact that serves as a warning.

com.github.apl-devs:appintroSimilarly, for com.github.jkcclemens:khttp, the user had vanished. We successfully claimed the username, recreated the repo, and secured the JitPack coordinates. In this case, the project seems pretty much dead, although after reviving the Jitpack environment, we have seen some attempts at contact.
Partial Takeover: JsBridge
The case of com.github.lzyzsd:jsbridge revealed the first layer of protection.
- Outcome: The user
lzyzsdwas available for registration, but the specific repository namejsbridgewas blocked by GitHub’s Namespace Retirement protection logic. - Mitigation: We successfully claimed the user handle
lzyzsd. While we cannot create the repository to serve a warning artifact, we have effectively mitigated any potential risk. By holding the user identity, we prevent any attacker from claiming the namespace chain.

Blocked Targets: ForgeGradle And Gruveo
Finally, we encountered targets where the platform protections worked more aggressively.
- Outcome: For
asbyth(ForgeGradle) andgruveo, we were unable to register the usernames. GitHub and Bitbucket returned them as unavailable, even though the accounts return a 404 status and are not currently in use. - Insight: This confirms that GitHub and Bitbucket have a mechanism to lock abandoned usernames, but its application isn’t uniform. Why was
apl-devsleft open whileasbythwas locked? We’re not entirely sure, but it’s a fact.
Ethical Note
All claimed namespaces (apl-devs, lzyzsd, jkcclemens) are held strictly for defensive purposes. We do not distribute functional code, and our sole intent is to prevent supply chain vectors while the ecosystem updates its references.
We will return any namespace to the rightful owners upon request to labs [at] itresit [dot] es.
6# Search for yourself
We built a toolkit named NecroJitPack containing two internal tools which we are releasing so that you can search for yourself.
JitPack Scanner (jitpack_scanner.py)
- Focus: Discovery & Validation.
- Logic: Parses Gradle files via GitHub Code Search and extracts JitPack coordinates (
com.github.*,org.bitbucket.*). - Validation: Checks whether the upstream namespace is live (200), redirected (301), or void (404).
- Enrichment: Queries MVNRepository to estimate the usage impact of dead coordinates.
Impact Analyzer (impact_analyzer.py)
- Focus: Exposure Posture.
- Logic: Samples public references to confirmed targets.
- Classification: Categorizes resolution behavior into risk levels:
- Critical: Snapshots (
-SNAPSHOT) or dynamic versions (+). - High: Mutable tags (e.g.,
v1.0). - Secure: Pinned commit hashes.
- Critical: Snapshots (
7# A Possible Inception Effect
Modern mobile projects are rarely just Gradle. Frameworks like React Native or Flutter often ship native Android configuration embedded inside other ecosystems.
The problem is a tooling gap:
- JS tooling scans
package.json. - Security scanners check NPM dependencies.
- But during the build, Gradle quietly resolves a dependency defined deep inside
node_modules/**/android/build.gradle.
We observed this in the wild with wrappers pulling native SDKs via org.bitbucket.gruveo. Regardless of Gruveo’s current status, the mechanism is the risk.
We haven’t explored this line of investigation in depth, but we leave it here as a side note. This mechanism creates a blind spot where neither npm audit nor standard Gradle checks usually look. A healthy, verified NPM package can act as a carrier for a dead, hijackable native dependency.
It’s a thread that might be worth pulling.
8# MitigatIONS
Prefer identifiers that don’t move
When we can, we prefer coordinates that don’t drift. In practice that often means pinning to commit hashes for JitPack dependencies. It is not always pretty but it reduces how much a name change can affect what gets built.
Add Local integrity Controls
Gradle supports dependency verification through verification-metadata.xml. It lets us lock checksums and optionally signatures for the artifacts we consume. The value is simple. If the bytes change, the build becomes loud instead of silent.
Keep an internal immutable cache
In enterprise setups, build-from-source repos are volatile upstreams. A common move is to put an internal proxy in front, like Nexus or Artifactory, and keep cached artifacts immutable once they enter the environment.
Reduce long-tail exposure
When we look for where this bites first, the same patterns show up:
- old
com.github.*coordinates pointing to abandoned namespaces - snapshots (
*-SNAPSHOT) - dynamic selectors (
1.+)
None of these are wrong by default. They just make time drift easier to turn into incidents.
9# Closing thought: the repojacking story we wanted, and the one we got
This isn’t a Click-to-Pwn exploit. Supply chain failures rarely look like an exploit on day one. They look like a boring rename. A repo moved. A maintainer cleaned up an account. Then, months or years later, the old name becomes real again.
In our opinion, this vector has all the ingredients to remain in the background, going unnoticed until someone finds the right namespace and decides to exploit it: it is obsolete technology, there are some security measures in place to prevent it, although they do not always work, and, in general, it has unattractive exploitation mechanics.
Once the investigation is complete, we can state that the successful exploitation of this vector requires a painful alignment of stars. You need a moved repository that GitHub (or others) failed to protect and a specific JitPack build status with open slots. You can’t overwrite a frozen artifact; so you have to hunt for the versions that failed or were never built.
Is the impact worth the effort? Maybe yes, maybe no.
But we have proven that sometimes the stars can align; all it takes is patience and perseverance. We proved it with AppIntro (and potentially in khttp).
Obviously, we didn’t find thousands of vulnerable libraries, but we did find one that is used in hundreds of projects, has thousands of monthly downloads on Jitpack and continues to be developed relatively actively today. If we’re talking about supply-chain security, one bullet may be enough.
Ultimately, old names in Jitpack’s ecosystem are not always truly dead. Sometimes they are simply waiting for their chance to be reborn. That’s the risk.
That’s supply-chain necromancy.
Disclosure Timeline
We follow standard coordinated disclosure practices. In this case, despite multiple attempts to contact the involved platforms regarding the identity persistence gaps, no response was received.
- Nov – Dec 2025: Initial research, impact analysis on the Android ecosystem, and development of validation tools.
- 2026-01-27: First notification sent to JitPack security team (
security@jitpack.io). - 2026-01-27: Notification sent to GitHub Security (
opensource-security@github.com) regarding the implications of namespace reuse in downstream build systems. - 2026-02-03: Follow-up notification sent to JitPack (
security@jitpack.io). - 2026-02-16: Defensive Takeover. We reclaimed the vulnerable GitHub namespaces identified in this report to prevent malicious exploitation after publication.
- 2026-02-18: At the time of publishing, neither JitPack nor GitHub have acknowledged the report or responded to coordination requests. The described behavior regarding namespace reuse and build state remains active.