TL;DR
- JFrog Artifactory is one of the most widely deployed systems for storing and distributing the software packages companies build and depend on. A flaw in how it verified membership of its own server cluster meant that anyone who could reach an internet-facing instance could simply ask for administrator access, and the server would grant it. Attackers began exploiting it within days of the flaw being disclosed. An Artifactory administrator can read every package an organization ships, upload malicious ones that downstream builds install as trusted, and retrieve credentials that reach the systems around it, so the damage does not stop at Artifactory.
- CVE-2026-82329 is a critical unauthenticated authentication bypass in self-managed JFrog Artifactory, rated CVSS 9.8. On a default install, JFrog Access registers a cluster join key whose id and signing secret are both derivable by anyone, so one forged join request to an endpoint that requires no authentication returns an admin-scoped token. Bishop Fox reproduced the full chain to Artifactory administrator against a default 7.111.20 instance and confirmed the fix on 7.111.21. Patch to 7.111.21, 7.117.28, 7.125.20, 7.133.29, 7.146.38, or 7.161.20. The CVE is KEV-listed with in-the-wild exploitation reported, and JFrog published no discovery credit.
Summary
On August 28, 2026, JFrog published an advisory for CVE-2026-82329, an authentication bypass in self-managed Artifactory that JFrog rates Critical at CVSS 9.8 and classifies as CWE-287. The advisory describes an "authentication weakness that, under default configuration, may allow an unauthenticated attacker with network access to obtain administrative privileges." The vulnerable component is JFrog Access, the authentication microservice bundled with every Artifactory deployment, and specifically its cluster-join subsystem.
Our research included:
- Class-level diff of the vulnerable (7.111.20) and patched (7.111.21) container images, and decompilation of the two changed classes
- Root cause analysis of the join-key resolution path in access-server-core and access-common-api
- End-to-end exploitation against a default-configuration lab instance, confirming an unauthenticated path to Artifactory administrator
- Development and validation of a non-invasive differential detection check against both vulnerable and patched instances
- Internal tooling for authorized impact validation, used to confirm exploitability on in-scope customer assets
Artifactory sits in the middle of a build pipeline as the authoritative store and proxy for the packages an organization consumes and produces. An administrator can read every artifact, publish new ones under trusted coordinates, retrieve the credentials Artifactory uses to reach upstream registries, and alter the repository definitions downstream builds pull from. CISA added CVE-2026-82329 to the Known Exploited Vulnerabilities catalog on September 2, 2026, with a remediation due date of September 5 and a forensic triage requirement, meaning patching alone does not discharge the obligation.
What Defenders Should Do Right Now
- Patch to the fixed release for your branch: 7.111.21, 7.117.28, 7.125.20, 7.133.29, 7.146.38, or 7.161.20. This is the only remediation. JFrog patched hosted Cloud environments directly.
- Treat any pre-patch internet-facing instance as potentially compromised. Vulnerable builds log
Adding join key with kid: e3b0c442...at WARN on every Access startup, which dates your exposure window. - Hunt the artifacts: HTTP 201 responses from
/access/api/v1/registry/joinfrom anything that is not a node you joined, non-expiring admin-scoped tokens, unexpected administrator accounts, and accounts flaggedartifactory_adminin custom data. - Rotate what an administrator could read: the join key, Access tokens, LDAP, proxy and mail credentials, and every upstream registry credential on a remote repository. When revoking a suspect account, confirm the login actually fails; Access reports deletion immediately, but Artifactory honors that account's basic-auth login for a cache window afterward.
Background
Affected self-managed versions are everything below 7.111.21, plus 7.117.0 through 7.117.27, 7.125.0 through 7.125.19, 7.133.0 through 7.133.28, 7.146.0 through 7.146.37, and 7.161.0 through 7.161.19. JFrog credited no outside reporter for the discovery.
Exploitation followed the August 28 advisory within days. On September 1, 2026, exposure management firm watchTowr reported observing attackers minting administrator tokens and enumerating users, groups, credential sets, and federated access topologies, with backdoor accounts created in a limited number of cases.
When an administrator adds a node to an Artifactory high-availability cluster, the joining node authenticates with a shared secret called the join key. A joining node has no user identity yet, so the endpoint that accepts a join request cannot require user authentication. It accepts any JWT whose signature matches a join key the server already holds. On a default install, one of those keys is the empty string, which is not a secret at all, so anyone can sign a join request that the server accepts as genuine.
The Vulnerability
On a default self-managed install, the additional-join-keys configuration value is unset, and resolveJoinKeys returns an empty string rather than an absent value (JoinKeyAccess.java). That empty string reaches the loop that registers each configured key:
if (!joinKey.isEmpty()) {
Arrays.stream(((String)joinKey.get()).split(",")).map(String::trim).forEach(jKey -> {
JoinKeyHashPair hashPair = new JoinKeyHashPair(jKey);
joinKeyListValuesForContext.put(hashPair.getHash(), hashPair);
Three checks stand between an unset config value and a registered key, and all three let it through. The guard reads !joinKey.isEmpty(), but joinKeyis a result wrapper, so it asks whether the lookup failed, not whether the string is empty. Splitting an empty string on a comma then returns one element, the empty string itself, so the loop body runs. And the constructor checks that the value is valid hex rather than that it exists, which an empty string satisfies (JoinKeyHashPair.java:16-19). Access registers the key under the SHA-256 of its own value:
kid = SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
The signing secret comes from that same empty value. Access hex-decodes the join key and PKCS7-pads the result to 32 bytes. PKCS7 fills padding with the number of bytes it adds, so padding nothing out to 32 bytes gives 32 bytes of the value 32, which is 0x20. Both the key id and the secret can be computed offline from the empty string.
The endpoint that consumes these keys is unauthenticated by design, and its implementing class is named accordingly: RegistryNoAuthResource. It takes a raw JWT as a text/plain body.
POST /access/api/v1/registry/join HTTP/1.1 Host: target:8082 Content-Type: text/plain <HS256 JWT, signed with 32x 0x20, claims: iat, kid, service_id, node_id, skip_node_registration>
Against a default 7.111.20 instance this returns HTTP 201 and a token decoding to:
{"iss":"jfrt@0175bc4fdf82a4","sub":"jfrt@0175bc4fdf82a4","scp":"admin",
"aud":"jfac@01m1pbnfvc70y01ewngptj1txz","iat":1788530695,"jti":"b3c985e7-59e9-4d89-bea3-df58f6263f43"}
The token carries scp: admin and no exp, because ServiceTokenProviderImpl#getToken builds it with .scope("admin").expiresIn(0). One unauthenticated request therefore buys a permanent Access administrator credential. Its audscopes it to Access alone, so reaching Artifactory took one further call, minting a token scoped applied-permissions/admin for audience *@*. That returned the full system configuration from GET/artifactory/api/system/configuration, which answers HTTP 401 without a token.
The Patch
Across the entire platform the fix changes two class files, both in Access. The primary change drops blank elements before any key object is constructed, and a constructor guard enforces the same invariant against future callers:
--- 7.111.20/org/jfrog/access/server/startup/JoinKeyAccess.java
+++ 7.111.21/org/jfrog/access/server/startup/JoinKeyAccess.java
- Arrays.stream(((String)joinKey.get()).split(",")).map(String::trim).forEach(jKey -> {
+ Arrays.stream(((String)joinKey.get()).split(",")).map(String::trim).filter(Strings::isNotBlank).forEach(jKey -> {
--- 7.111.20/org/jfrog/access/token/JoinKeyHashPair.java
+++ 7.111.21/org/jfrog/access/token/JoinKeyHashPair.java
public JoinKeyHashPair(String joinKey) {
+ if (joinKey == null || joinKey.isBlank()) {
+ throw new IllegalArgumentException("Join key must not be null or blank");
+ }
KeyUtils.validateHexEncoding((String)joinKey);
We verified this in bytecode rather than by version string. Artifactory 7.111.20 ships Access 7.141.17, whose JoinKeyAccess class carries no isNotBlank reference and whose JoinKeyHashPair lacks the guard string; 7.111.21 ships Access 7.141.18 with both present. On the patched instance the empty kid is rejected at lookup with The kid:e3b0c442...supplied doesn't match any join keys that this server has knowledge of.
Detection
Confirming this vulnerability outright means exploiting it via minting a non-expiring administrator token on the target. While this is a conclusive true-positive check, we preferred not to exploit this across customer attack surfaces at scale, where the check itself would leave a record of that token on every vulnerable host it touched, indistinguishable from one an attacker minted.
So we went looking for an indicator that stops short of exploitation. We found that the server's error message reveals whether it recognizes the empty key without the need for a valid signature. The check is two requests, both signed wrong.
The first names the empty kid. A server holding that key finds it, fails verification, and returns a signature mismatch. A patched server reports the key as unknown. The second is a control carrying a decoy kid registered nowhere, because some Access builds check any unrecognized kid against the main join key and return a signature mismatch to everything, patched or not. When the two answers differ, the empty key is registered, indicating a vulnerable instance.
Host | empty | decoy control | Reading |
Vulnerable 7.111.20 | signature does not match | doesn't match any join keys | vulnerable |
Patched 7.111.21 | doesn't match any join keys | doesn't match any join keys | not vulnerable |
Access 7.128.x | signature does not match | signature does not match | inconclusive |
Both tokens are static strings. The signature is invalid and iatis a fixed future date, so there is nothing to sign, no clock to read, and no way for either request to make the server mint a token:
# Probe: kid = SHA-256("") = e3b0c442... Control: a decoy kid registered nowhere
TOK_EMPTY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjIwMDAwMDAwMDAsImtpZCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJzZXJ2aWNlX2lkIjoiamZydEAwMXByb2JlIiwibm9kZV9pZCI6InByb2JlIiwic2tpcF9ub2RlX3JlZ2lzdHJhdGlvbiI6dHJ1ZX0.aW52YWxpZA
TOK_DECOY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjIwMDAwMDAwMDAsImtpZCI6ImJkZWI5YmEyMmFmOGZhNzNlNTlmZTdjNGQzYzQ4YWUxMTY1NjE3ZGQ3NmM3MjA3NzNjZGY2Y2JjMzNhOTFkZDciLCJzZXJ2aWNlX2lkIjoiamZydEAwMXByb2JlIiwibm9kZV9pZCI6InByb2JlIiwic2tpcF9ub2RlX3JlZ2lzdHJhdGlvbiI6dHJ1ZX0.aW52YWxpZA
# Access answers through the router, port 8082 by default, not the 8081 UI port
for T in "$TOK_EMPTY" "$TOK_DECOY"; do
curl -sk -X POST "https://artifactory.example.co..." \
-H 'Content-Type: text/plain' -d "$T" | grep -E 'message|detail'
done
A vulnerable host answers the two probes differently. A patched host gives both the same answer:
VULNERABLE (the two answers differ) probe "message" : "JWT's signature does not match the server's join key (join key mismatch)." control "message" : "The kid:bdeb9ba2... supplied doesn't match any join keys that this server has knowledge of " PATCHED (both answers identical) probe "message" : "The kid:e3b0c442... supplied doesn't match any join keys that this server has knowledge of " control "message" : "The kid:bdeb9ba2... supplied doesn't match any join keys that this server has knowledge of "
Conclusion
CVE-2026-82329 has a simple lesson: check that a secret exists, not just that it is well formed. Access validated the join key's encoding but never its presence, so an unset configuration value became a signing secret that anyone could reproduce.
If you run self-managed Artifactory, patch now, and because this CVE is KEV-listed with confirmed exploitation, audit for the artifacts above rather than assuming the upgrade closed the story.
Adversarial Operations validated each finding, and Cosmos customers were notified about this vulnerability research shortly after the vendor advisory published. If you are interested in learning more about managed services delivered through our Cosmos platform, visit bishopfox.com/services/continuous-threat-exposure-management.
Subscribe to our blog
Be first to learn about latest tools, advisories, and findings.
Thank You! You have been subscribed.
Recommended Posts