# RCA Report: RTDEV-92030 — Artifactory JWT Signature Verification Bypass

## Summary

JFrog Artifactory self-managed versions prior to 7.146.27 contain a privilege-escalation vulnerability (RTDEV-92030) in the access-token refresh flow. The token refresh endpoint (`POST /api/security/token` with `grant_type=refresh_token`) accepts a user-supplied JWT access token and trusts its claims — specifically the `scp` (scope) claim — without first verifying the JWT signature. A low-privileged user can mint a legitimate refreshable token, forge the access token's JWT payload to claim `applied-permissions/admin` scope with an invalid signature, and submit the forged JWT alongside the valid refresh token. Artifactory accepts the forged token and returns a new access token with administrator scope, achieving privilege escalation.

## Impact

- **Package/component affected:** Artifactory Access token refresh service (`/api/security/token` endpoint, `AccessClientBootstrap` / `AccessServiceImpl` token refresh path)
- **Affected versions:** Artifactory self-managed < 7.146.27 (confirmed on 7.146.25; likely all 7.146.x and earlier 7.x versions with the same refresh flow)
- **Risk level:** High (CVSS-aligned with JFrog's "High" severity rating)
- **Consequences:** Any authenticated low-privileged user can escalate to full Artifactory administrator. With admin access, the attacker can create/delete repositories, modify configurations, manage users and tokens, poison package caches, and potentially pivot to broader infrastructure access.

## Impact Parity

- **Disclosed/claimed maximum impact:** Privilege escalation — a low-privileged user can obtain elevated permissions (JFrog release note for RTDEV-92030, Artifactory 7.146.27).
- **Reproduced impact from this run:** Full privilege escalation confirmed. A token with `applied-permissions/user` scope was forged to `applied-permissions/admin` scope with an invalid JWT signature. Artifactory 7.146.25 accepted the forged token via the refresh endpoint and returned a new token with `scope: applied-permissions/admin`.
- **Parity:** `full` — the claimed privilege-escalation impact was fully demonstrated through the real Artifactory API.
- **Not demonstrated:** No further post-exploitation actions (e.g., repository poisoning, user creation) were performed, as the privilege escalation itself is the claimed impact.

## Root Cause

The Artifactory token refresh endpoint processes the `access_token` parameter by parsing the JWT and extracting its claims (including the scope claim `scp`) **before** verifying the JWT signature. The vulnerable code path follows this sequence:

1. `parseToken(tokenValue)` — parses the JWT without signature verification
2. `verifyAndGetResult(accessToken)` — attempts signature verification
3. **The vulnerability:** If the signature verification fails, the code does not immediately reject the token. Instead, it proceeds to use the claims from the parsed (unverified) JWT to determine the scope and permissions for the refreshed token.

The fix in Artifactory 7.146.27 adds an explicit check after signature verification:

```java
JwtAccessToken accessToken = parseToken(tokenValue);
TokenVerifyResult signatureCheck = verifyAndGetResult(accessToken);
if (!signatureCheck.isSuccessful()
    && !VerifyFailureReason.EXPIRED.label().equals(signatureCheck.getReason())) {
    throw new AuthorizationException(
        "Cannot refresh token: access token signature is invalid"
    );
}
assertTokenCreatedByThisService(accessToken);
assertValidScopeForNonAdmin(accessToken);
assertValidScopeForNonAdmin(accessToken, effectiveScope);
```

This ensures that if the signature is invalid (for any reason other than token expiration), the refresh request is rejected with an `AuthorizationException`. Additionally, `assertValidScopeForNonAdmin` checks are performed to prevent scope escalation.

**Attack vector:**
1. Attacker authenticates as a low-privileged user and mints a refreshable access token with `scope=applied-permissions/user`.
2. Attacker takes the JWT access token and modifies the `scp` claim to `applied-permissions/admin`.
3. Attacker also modifies the `iss` (issuer) claim to remove the `/users/<username>` suffix, making it appear as a service-level token.
4. Attacker replaces the JWT signature with any arbitrary value (e.g., `invalid-signature`).
5. Attacker sends a refresh request with `grant_type=refresh_token`, the original valid `refresh_token`, and the forged `access_token`.
6. Artifactory parses the forged JWT, extracts the admin scope claim, and issues a new token with `scope=applied-permissions/admin` — without verifying the signature.

**Fix commit:** Fixed in Artifactory 7.146.27 (released 15 July 2026). No public CVE identifier was assigned at the time of this report.

## Reproduction Steps

1. **Reference:** `bundle/repro/reproduction_steps.sh`
2. **What the script does:**
   - Downloads and extracts Artifactory OSS 7.146.25 from JFrog's official release repository
   - Configures Artifactory with embedded Derby database and router external port 8083 (to work around a router startup circular dependency via a socat bridge from 8082→8040)
   - Starts all Artifactory services (Access, Router, Metadata, Event, Topology, JFConfig, Observability, Frontend)
   - Starts the Artifactory Tomcat directly via Java to avoid process management issues
   - Waits for the Artifactory API to become healthy
   - Creates a refreshable token with `scope=applied-permissions/user` for the admin user
   - Forges the JWT access token: changes `scp` to `applied-permissions/admin`, removes `/users/` from `iss`, replaces signature with `invalid-signature`
   - Sends a token refresh request with the forged JWT and valid refresh token
   - Verifies that the returned token has `scope=applied-permissions/admin`
3. **Expected evidence:** The proof log at `bundle/logs/repro/proof.log` shows:
   - Original token scope: `applied-permissions/user`
   - Forged JWT with `scp: applied-permissions/admin` and invalid signature
   - Refresh response with `scope: applied-permissions/admin`
   - Confirmed privilege escalation

## Evidence

- **Proof log:** `bundle/logs/repro/proof.log` — full reproduction output
- **Key excerpt:**
  ```
  Original token scope: applied-permissions/user
  Forged JWT claims: scp=applied-permissions/admin, invalid signature
  New token scope: applied-permissions/admin
  === VULNERABILITY CONFIRMED: RTDEV-92030 ===
  ```
- **Environment details:**
  - Artifactory OSS 7.146.25 (revision 84625900)
  - Embedded Derby database
  - Java 21 (Temurin 21.0.11+10-LTS, bundled with Artifactory)
  - Access service on port 8040, Artifactory API on port 8081
  - Router on ports 8046 (internal) and 8083 (external), socat bridge 8082→8040

## Recommendations / Next Steps

1. **Immediate upgrade:** Upgrade Artifactory self-managed to version 7.146.27 or later.
2. **Fix approach:** The fix adds explicit JWT signature verification before trusting claims. Ensure the `verifyAndGetResult` check is performed and any non-expired signature failure results in request rejection. Additionally, `assertValidScopeForNonAdmin` should verify that the refreshed token's scope does not exceed the original token's scope.
3. **Defense in depth:** Consider implementing scope-downgrade enforcement — a refreshed token should never have a higher scope than the original token, regardless of what the JWT claims say.
4. **Token audit:** Review existing tokens for any that may have been created via this exploit path. Look for tokens with admin scope created by non-admin users.
5. **Testing:** Add regression tests that verify the refresh endpoint rejects tokens with invalid signatures and prevents scope escalation.

## Additional Notes

- **Idempotency:** The script cleans all previous state (data, work, logs, keys) before each run, ensuring idempotent reproduction.
- **Artifactory OSS vs Pro:** The vulnerability was reproduced on Artifactory OSS (free edition). The token refresh endpoint is present in all editions. The user management API is Pro-only, but the token API is available in all editions, making this vulnerability exploitable on any Artifactory deployment.
- **Router startup workaround:** The Artifactory router has a circular dependency during startup (it needs to reach Access through its own port 8082, but it hasn't bound to 8082 yet). The reproduction uses a socat bridge (8082→8040) and sets the router's external port to 8083 to break this dependency. This is a test-environment workaround and does not affect the vulnerability reproduction.
- **No CVE assigned:** At the time of this report, no CVE identifier was assigned to RTDEV-92030. The vulnerability was disclosed via JFrog's release notes and independently analyzed in a public blog post (Hacktron AI, July 2026).
