# Patch Analysis — CVE-2026-82329 (JFrog Artifactory unauthenticated auth bypass via blank join key)

## What the fix changes

Binary diff `artifactory-jcr:7.146.36` (last vulnerable) → `artifactory-jcr:7.146.38` (fixed). The entire payload delta is the bundled Access service (7.176.27 → 7.176.28). Exactly two security-relevant classes changed (verified in this stage against the 7.146.25/7.146.38 extracted jars as well — `JoinServiceImpl`, `RegistryNoAuthResource`, `RegistryResource`, `SecuritySubResource` are byte-identical across versions):

1. **`org/jfrog/access/server/startup/JoinKeyAccess.class`** — `tryResolveJoinKeys()`:
   ```diff
   - Arrays.stream(joinKey.get().split(",")).map(String::trim).forEach(jKey -> {
   + Arrays.stream(joinKey.get().split(",")).map(String::trim).filter(Strings::isNotBlank).forEach(jKey -> {
   ```
   Blank entries from `shared.security.additionalJoinKeys` (default value: empty string → `[""]`) are no longer turned into `JoinKeyHashPair`s.

2. **`org/jfrog/access/token/JoinKeyHashPair.class`** (in `access-common-api`) — constructor:
   ```diff
   + if (joinKey == null || joinKey.isBlank()) {
   +     throw new IllegalArgumentException("Join key must not be null or blank");
   + }
   ```
   Defense-in-depth: blank keys can no longer be constructed even from other call paths.

## Assumptions the fix makes

- **Single choke point**: every consumer of join keys obtains them via `JoinKeyAccess.getTokenSignatureVerifiers(...)` / `JoinKeyHashPair`. Verified true in this stage: constant-pool scan across all Access server jars shows `JoinKeyUtils.getSigningKey` is called only from `JoinServiceImpl`, and `JoinService` is referenced only by `RegistryNoAuthResource` (network) and `TopologyServiceImpl` (internal). No gRPC service, no other REST resource, and no other startup component consumes join keys.
- **Blank-ness is the whole bug class**: the empty-default config can only produce blank keys; trimmed whitespace entries collapse to blank. Verified: `split(",")` + `trim` on `""`, `" "`, `",,"` etc. all yield blank strings, all now filtered/rejected.
- The main (random) join key resolution path is unchanged and unaffected.

## Code paths / inputs the fix does NOT cover

- **Low-entropy non-blank join keys**: `JoinKeyUtils.getSigningKey` pkcs7-pads any short hex key to 32 bytes deterministically (e.g. configured key `00` → `0x00` + 31×`0x1f`). Still possible, but only via administrator-controlled configuration — no attacker trust-boundary crossing, hence not a variant of this CVE (which is defined by zero-credential network exploitation of the *default* config).
- Nothing else: both unauthenticated entry points (`/v1/registry/join`, `/v1/registry/join/router`) and both key-selection branches (no-kid try-all, explicit-kid lookup in `getRelevantJoinKeys`) funnel into the patched choke point.

## Behavior before vs after (verified at runtime in this stage)

| Request | 7.146.25 (vuln) | 7.146.38 (fixed) |
|---|---|---|
| `POST /access/api/v1/registry/join` (no kid, blank-key JWT) — parent PoC | 201, `scp=admin` service token | 400 |
| `POST /access/api/v1/registry/join/router` (blank-key JWT) — variant A | 200, wrapper JWT with admin token + root CA cert | 400 |
| `POST /access/api/v1/registry/join/router?override=true` — variant B | 200, same | 400 |
| `POST /access/api/v1/registry/join` with `kid=sha256("")` — variant C | 201, `scp=admin` service token | 400 |
| Anonymous `POST /access/api/v1/tokens` (control) | 401 | 401 |
| Wrong-signature join (control) | 400 | 400 |

## Completeness verdict

The fix is **complete for this root cause**. All discovered variants are blocked on the fixed build. The only residual hardening gaps (minimum key entropy, mandatory `kid`, join-rate limiting) are improvements, not holes in the patch.

## Target threat-model context

JFrog Artifactory is a self-hosted network service; its join endpoint is explicitly designed as a *no-auth* bootstrap API whose sole authentication factor is possession of the join key secret. The vendor rated this CVE 9.8 PR:N, confirming that unauthenticated network reachability of `/access/api/v1/registry/*` with a derivable key is squarely in-scope of its threat model (contrast with projects that declare local-file-parsing bugs out of scope). The variants found here cross the exact same trust boundary (unauthenticated network → token issuance) via the same sink family, so they are legitimate variants; none of the tested behavior is "documented/by-design" functionality.
