# CVE-2026-48558 — Verification Report (Coding Stage)

## Fix Summary

SimpleHelp's OpenID Connect technician-login flow accepted an OIDC ID token and
trusted its claims **without verifying the token's cryptographic signature**.
In `OIDCAuthenticator.registerResponse(...)` the token was parsed with
`new IDToken(token)` and `loginCallback.oidcSuccess()` was called after only a
state/nonce check — neither of which validates the signature — so an
unauthenticated remote attacker could submit a forged JWT (e.g. `alg:none`) and
obtain a fully authenticated technician session (CVE-2026-48558).

The fix adds a new `utils.oauth.oidc.IDTokenVerifier` that cryptographically
verifies the ID token's signature (using signing keys resolved from the
**configured** provider metadata/JWKS, never from the token itself) and
validates the standard claims (`alg`, `iss`, `aud`, `exp`, `iat`, `nonce`)
**before** any claim is trusted. `OIDCAuthenticator` now calls
`IDTokenVerifier.verify(...)` immediately after parsing the token and fails
closed (`oidcFailed`) on any verification error, so no technician session is
created from a forged identity. Unsigned (`alg:none`) and incorrectly signed
tokens are rejected, matching the patched 5.5.16 behavior.

## Changes Made

This is a commercial binary distribution (encrypted `com.aem.*` class files, no
public source). The fix is expressed against the decompiled source and deployed
to the binary as follows.

| Artifact | Change | Purpose |
|---|---|---|
| `utils/oauth/oidc/IDTokenVerifier.java` | **NEW** | Verifies JWT signature via JWKS + validates `alg`/`iss`/`aud`/`exp`/`iat`/`nonce`. Rejects `alg:none`/unsigned/unsupported algorithms. Fail-closed. JWKS cached with TTL. Supports RS/PS/ES algorithms. |
| `com/aem/shelp/proxy/authentication/oidc/OIDCAuthenticator.java` | **MODIFIED** | After `new IDToken(token)`, calls `IDTokenVerifier.verify(token, idToken, provider, metadata)`; on `Throwable` calls `oidcFailed(...)` and returns `false` **before** `oidcSuccess()`. Removed the `com.aem.CentralDebugging` dependency (debug-only verbose logging) so the class can be recompiled standalone. Added an explicit fail-closed guard for a null token. All public methods/signatures preserved for binary compatibility. |
| `FixAgent.java` + `fixagent.jar` | **NEW (deployment)** | Java agent that substitutes the fixed `OIDCAuthenticator` bytes at class-load time (see Deployment below). |

### Core diff (see `bundle/coding/proposed_fix.diff`)

In `OIDCAuthenticator.registerResponse`:

```java
this.idToken = new IDToken(token);

// CVE-2026-48558 fix: verify signature + claims before trusting the token.
try {
    IDTokenVerifier.verify(token, this.idToken, this.provider, this.metadata);
} catch (Throwable t) {
    t.printStackTrace();
    this.loginCallback.oidcFailed("Invalid response - ID token signature verification failed");
    return false;
}

String returnedNonce = this.idToken.getNonce();
if (!this.metadata.matches(returnedNonce)) { ... }
this.loginCallback.oidcSuccess();
```

`IDTokenVerifier.verify` rejects `alg:none` explicitly and requires a real
signature verified against JWKS keys resolved from the configured provider:
- Generic OIDC → `OIDCAuthenticationProvider.getDiscoveryURL()` → well-known
  `issuer` + `jwks_uri`.
- Azure/Entra → `AzureAuthenticationProvider.getTenantID()` → Microsoft
  `discovery/v2.0/keys`.

### Deployment (binary patching)

SimpleHelp's `SecureRunner`/`SHClassLoader` encrypts all `com.aem.*` classes in
`simplehelp.jar` and decrypts them at load time (`isProtectedClass` matches
`com.aem`/`com/aem`). A plain replacement placed in the jar would be
re-"decrypted" and corrupted. Two mechanisms are therefore used:

1. `IDTokenVerifier` (and inner classes) are added **directly to
   `simplehelp.jar`** under `utils/oauth/oidc/`. The `utils.*` package is
   **not** protected, so `SHClassLoader` loads these as plain class files
   (exactly like the existing `utils.oauth.oidc.IDToken`).
2. `OIDCAuthenticator` is an encrypted `com.aem.*` class, so the fixed class is
   injected at load time by the `FixAgent` Java agent
   (`-javaagent:fixagent.jar`). Its `ClassFileTransformer` returns the fixed
   bytes for `com/aem/shelp/proxy/authentication/oidc/OIDCAuthenticator` after
   the classloader has decrypted the original (the same mechanism the repro
   agents used to dump decrypted classes). `IDTokenVerifier` is resolvable by
   `SHClassLoader` from the patched jar.

The fixed classes are compiled to Java 8 class version 52 to match the original
artifacts, and run on the bundled JRE 21.

## Verification Steps

`bundle/coding/verify_fix.sh` (idempotent) performs the end-to-end verification
against the **real** SimpleHelp 5.5.15 server binary:

1. Extracts a clean SimpleHelp 5.5.15 server (SHA256
   `26cd904e...`) from the repro-downloaded tarball.
2. Patches `simplehelp.jar` with `IDTokenVerifier` and confirms the classes are
   present.
3. First-launch initialises `serverconfig.xml`; an OIDC provider
   (`http://127.0.0.1:8080`, `client_id=test-client-id`) and a non-admin
   `Technicians` group with OIDC group-authenticated logins are configured
   (mirrors the repro configuration).
4. Starts a local fake OIDC IdP that serves a well-known document (`jwks_uri`
   included) and, at `/token`, returns a **forged `alg:none` ID token** with a
   bogus signature segment and attacker-controlled claims.
5. Starts the patched server **with** `-javaagent:fixagent.jar=<evidence.log>`.
6. Runs the genuine flow: `GET /auth/v1/account/oidc_get` → follow the IdP
   authorization URL → `GET /oidc?code=fakecode&state=<server state>` (server
   exchanges the code and receives the forged `id_token`) →
   `GET /auth/v1/account/status`.
7. Writes a verdict JSON (`verify_logs/verify_result.json`) and an evidence log.

Commands run (abridged):
```
bash bundle/coding/verify_fix.sh
```

## Test Results

**Result: PASS** (`verify_logs/verify_result.json` → `"PASS": true`).

```json
{
  "fix_agent_premain_loaded": true,
  "fix_agent_applied": true,
  "verifier_invoked": true,
  "verifier_rejected_algnone": true,
  "verifier_rejected_any": true,
  "verifier_ok": false,
  "forged_token_rejected": true,
  "callback_login_failed": true,
  "status_unauthenticated": true,
  "status_fully_authenticated": false,
  "flow_error": null,
  "PASS": true
}
```

Evidence log (`verify_logs/fix_evidence.log`) — proof survives SimpleHelp's
post-startup stdout redirection because the agent/verifier write to a file:
```
[FixAgent] FixAgent premain: fixed OIDCAuthenticator resource loaded (5715 bytes)
[FixAgent] AGENT_APPLIED: replaced OIDCAuthenticator with CVE-2026-48558 fixed version (loader=SHClassLoader@...)
[IDTokenVerifier] REJECTED: ID token alg is 'none' (unsigned/forged token)
```

HTTP flow (`verify_logs/fixed_flow.json`):
- `/auth/v1/account/oidc_get` → 200, returns the real authorization URL with a
  server-issued `state`.
- The IdP `/token` endpoint **was contacted** and returned the forged
  `alg:none` token (confirmed in `fixed_idp.log`: `FORGED_ID_TOKEN ...`).
- `/oidc?code=fakecode&state=...` → 200, body contains **"Login Failed"**
  (not "Login Complete").
- `/auth/v1/account/status` →
  `{"state":"UNAUTHENTICATED","code":0}`.

### Before/after parity

| | 5.5.15 unpatched (repro) | 5.5.15 + this fix | 5.5.16 vendor (repro) |
|---|---|---|---|
| Forged `alg:none` token | `FULLY_AUTHENTICATED` | `UNAUTHENTICATED` | `UNAUTHENTICATED` |
| `/oidc` callback | Login Complete | Login Failed | Login Failed |

The fixed 5.5.15 behaves identically to the vendor-patched 5.5.16 for the
forged-token attack, and the negative control (agent applied, verifier
rejected `alg:none`, no session) is explicitly evidenced.

### Edge cases considered
- **Both OIDC ingestion paths**: the fix is in `OIDCAuthenticator.registerResponse`,
  which is the common sink for both the generic authorization-code flow and the
  Azure/Entra direct `id_token` form-post flow (per the variant analysis). Both
  route through this method before `oidcSuccess()`.
- **Fail-closed**: any exception in `IDTokenVerifier.verify` (bad JWT, missing
  JWKS, unsupported alg, bad signature, expired, wrong iss/aud/nonce) results in
  `oidcFailed` and no session — the server never trusts claims from an
  unverified token.
- **JWKS source trust**: keys/issuer are resolved from **server configuration**
  (discovery URL / Azure tenant), not from the token's own `iss`, so an
  attacker cannot redirect key resolution.
- Binary compatibility: all public methods/fields of `OIDCAuthenticator` are
  preserved; class version matches the original (52).

## Remaining Concerns / Limitations

- **Binary distribution**: there is no upstream source commit; this fix is
  applied to the vendor binary. The canonical remediation remains upgrading to
  SimpleHelp 5.5.16+ / the fixed 6.0 release. This patch is an interim
  mitigation for deployments that cannot immediately upgrade.
- **Agent-based deployment**: the fixed `OIDCAuthenticator` is injected via a
  Java agent (`-javaagent`) because the original is an encrypted `com.aem.*`
  class. Production deployment must ensure the agent jar and the
  `IDTokenVerifier`-patched `simplehelp.jar` are installed together and that
  the server is started with the `-javaagent` flag (e.g. via `serverstart.sh`).
- **Unverified paths (not exhaustively tested here)**: OIDC admin
  configuration-test UI flows and direct-machine/remote-work OIDC paths that
  may reuse these provider classes share the same root cause and should be
  covered by regression tests that submit unsigned / incorrectly-signed /
  wrong-iss / wrong-aud / expired ID tokens through the real `/oidc` callback
  and assert rejection. The variant analysis found no fixed-version bypass.
- **HS\* (symmetric) algorithms**: intentionally not accepted by
  `IDTokenVerifier`; OIDC ID tokens are expected to use asymmetric signatures
  verified via JWKS. Deployments relying on HS-signed ID tokens would need an
  extension.
- Additional hardening: enforce `exp`/`nbf` strictly, pin allowed issuers per
  provider, add JWKS key rotation tests, and audit for OIDC-created group
  technician accounts.
