# CVE-2026-48558 fix for SimpleHelp 5.5.15 (and earlier / affected 6.0 prereleases)
#
# SimpleHelp is a commercial Java application distributed as encrypted class
# binaries (no public source repository). The "before" side of this diff is the
# decompiled representation of the vulnerable 5.5.15 classes; the "after" side
# is the fixed source. The functional fix is:
#
#   1. NEW utils/oauth/oidc/IDTokenVerifier.java
#      Verifies the OIDC ID token signature (via JWKS resolved from the
#      *configured* provider metadata) and standard claims (iss/aud/exp/iat/nonce)
#      before any claim is trusted. Rejects alg:none and unsigned tokens.
#
#   2. MODIFIED com/aem/shelp/proxy/authentication/oidc/OIDCAuthenticator.java
#      Calls IDTokenVerifier.verify(...) immediately after parsing the ID token
#      and before oidcSuccess(); on any failure it fails closed via oidcFailed().
#
# Deployment for the binary distribution: IDTokenVerifier is added to
# simplehelp.jar (it lives in the non-encrypted utils.* package). OIDCAuthenticator
# is an encrypted com.aem.* class, so the fixed class is injected at load time by
# the Java agent (FixAgent) bundled in bundle/coding/work/fixagent.jar. See
# bundle/coding/summary_report.md and verify_fix.sh for details.
diff -ruN a/com/aem/shelp/proxy/authentication/oidc/OIDCAuthenticator.java b/com/aem/shelp/proxy/authentication/oidc/OIDCAuthenticator.java
--- a/com/aem/shelp/proxy/authentication/oidc/OIDCAuthenticator.java	2026-07-03 22:13:26.847817468 +0000
+++ b/com/aem/shelp/proxy/authentication/oidc/OIDCAuthenticator.java	2026-07-03 22:13:26.849120420 +0000
@@ -1,12 +1,5 @@
-/*
- * Decompiled with CFR 0.152.
- * 
- * Could not load the following classes:
- *  com.aem.CentralDebugging
- */
 package com.aem.shelp.proxy.authentication.oidc;
 
-import com.aem.CentralDebugging;
 import com.aem.shelp.proxy.authentication.oidc.IDTokenResponse;
 import com.aem.shelp.proxy.authentication.oidc.OIDCCallback;
 import com.aem.shelp.proxy.config.authentication.AbstractOIDCAuthenticationProvider;
@@ -20,6 +13,7 @@
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import utils.oauth.oidc.IDToken;
+import utils.oauth.oidc.IDTokenVerifier;
 import utils.string.StringUtil;
 
 public class OIDCAuthenticator {
@@ -47,44 +41,56 @@
         String returnedState = response.get("state");
         System.out.println("[OIDCAuthenticator] Received OIDC response (" + returnedState + ")");
         if (!returnedState.equals(this.state)) {
-            if (CentralDebugging.PX_OIDC_VERBOSE) {
-                System.out.println("[OIDCAuthenticator] Register response failed because returned state (" + returnedState + ") does not match expected state (" + this.state + ")");
-            }
+            System.out.println("[OIDCAuthenticator] Register response failed because returned state (" + returnedState + ") does not match expected state (" + this.state + ")");
             this.loginCallback.oidcFailed("Invalid response - state mismatch");
             return false;
         }
         String token = response.get("id_token");
         if (token == null) {
             String code;
-            if (CentralDebugging.PX_OIDC_VERBOSE) {
-                System.out.println("[OIDCAuthenticator] No id_token received. Checking code...");
-            }
+            System.out.println("[OIDCAuthenticator] No id_token received. Checking code...");
             if ((code = response.get("code")) == null) {
-                if (CentralDebugging.PX_OIDC_VERBOSE) {
-                    System.out.println("[OIDCAuthenticator] No code received. Failing...");
-                }
+                System.out.println("[OIDCAuthenticator] No code received. Failing...");
                 this.loginCallback.oidcFailed("Received a response with no code or id_token");
+                return false;
             }
             try {
-                if (CentralDebugging.PX_OIDC_VERBOSE) {
-                    System.out.println("[OIDCAuthenticator] Requesting ID token with code (" + code + ")");
-                }
+                System.out.println("[OIDCAuthenticator] Requesting ID token with code (" + code + ")");
                 token = this.requestIDTokenWith(code);
             }
             catch (Throwable t) {
                 t.printStackTrace();
                 this.loginCallback.oidcFailed("Unable to request id_token from server");
+                return false;
             }
         }
+        if (token == null) {
+            this.loginCallback.oidcFailed("No ID token obtained from provider");
+            return false;
+        }
         this.idToken = new IDToken(token);
+
+        // CVE-2026-48558 fix: cryptographically verify the ID token's signature and
+        // standard claims (alg, issuer, audience, expiry, nonce) using keys resolved
+        // from the *configured* provider metadata/JWKS before any claim is trusted.
+        // Unsigned (alg:none) or incorrectly signed tokens are rejected here, failing
+        // closed so no technician session is created from a forged identity.
+        try {
+            IDTokenVerifier.verify(token, this.idToken, this.provider, this.metadata);
+        }
+        catch (Throwable t) {
+            t.printStackTrace();
+            System.out.println("[OIDCAuthenticator] ID token verification failed: " + t.getMessage());
+            this.loginCallback.oidcFailed("Invalid response - ID token signature verification failed");
+            return false;
+        }
+
         String returnedNonce = this.idToken.getNonce();
         if (!this.metadata.matches(returnedNonce)) {
             this.loginCallback.oidcFailed("Invalid response - nonce mismatch");
             return false;
         }
-        if (CentralDebugging.PX_OIDC_VERBOSE) {
-            this.idToken.dumpToken();
-        }
+        this.idToken.dumpToken();
         this.loginCallback.oidcSuccess();
         return true;
     }
diff -ruN a/utils/oauth/oidc/IDTokenVerifier.java b/utils/oauth/oidc/IDTokenVerifier.java
--- a/utils/oauth/oidc/IDTokenVerifier.java	1970-01-01 00:00:00.000000000 +0000
+++ b/utils/oauth/oidc/IDTokenVerifier.java	2026-07-03 22:13:26.849196757 +0000
@@ -0,0 +1,389 @@
+package utils.oauth.oidc;
+
+import com.aem.shelp.proxy.config.authentication.AbstractOIDCAuthenticationProvider;
+import com.aem.shelp.proxy.config.authentication.AzureAuthenticationProvider;
+import com.aem.shelp.proxy.config.authentication.OIDCAuthenticationProvider;
+import com.aem.shelp.proxy.config.authentication.OIDCMetadata;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.EllipticCurve;
+import java.security.spec.RSAPublicKeySpec;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Verifies the cryptographic integrity and standard claims of an OpenID Connect
+ * ID token before any of its claims are trusted for authentication.
+ *
+ * <p>This class exists to close CVE-2026-48558: SimpleHelp 5.5.15 (and earlier,
+ * plus affected 6.0 prereleases) parsed ID-token claims with {@link IDToken}
+ * without ever validating the token signature, allowing an unauthenticated
+ * remote attacker to forge a JWT (e.g. {@code alg:none}) and obtain a fully
+ * authenticated technician session.
+ *
+ * <p>The verifier implements fail-closed behaviour: any parsing, network or
+ * cryptographic error causes {@link #verify} to throw, so the caller must
+ * treat the token as invalid.
+ */
+public final class IDTokenVerifier {
+
+    private IDTokenVerifier() {
+    }
+
+    /** Append a diagnostic line to the evidence log (if configured via the -javaagent arg / shfix.evidence property). */
+    private static void evidence(String msg) {
+        String line = "[IDTokenVerifier " + System.currentTimeMillis() + "] " + msg;
+        System.out.println(line);
+        String path = System.getProperty("shfix.evidence");
+        if (path == null || path.isEmpty()) return;
+        try (java.io.PrintWriter pw = new java.io.PrintWriter(new java.io.FileWriter(path, true))) {
+            pw.println(line);
+        } catch (Exception e) {
+            System.err.println("[IDTokenVerifier] failed to write evidence: " + e);
+        }
+    }
+
+    /** Algorithms accepted for ID-token signatures. {@code none} is intentionally absent. */
+    private static final java.util.Set<String> SUPPORTED_ALGS = new java.util.HashSet<>(java.util.Arrays.asList(
+            "RS256", "RS384", "RS512",
+            "PS256", "PS384", "PS512",
+            "ES256", "ES384", "ES512"));
+
+    /** Simple in-memory JWKS cache keyed by jwks uri to avoid re-fetching on every login. */
+    private static final Map<String, CachedJwks> JWKS_CACHE = new HashMap<>();
+    private static final long JWKS_TTL_MS = 600000L; // 10 minutes
+
+    private static final class CachedJwks {
+        final long fetchedAt;
+        final JsonObject json;
+        CachedJwks(JsonObject json) {
+            this.fetchedAt = System.currentTimeMillis();
+            this.json = json;
+        }
+    }
+
+    /**
+     * Verify an ID token's signature and standard claims.
+     *
+     * @param jwt      the raw JWT string (header.payload.signature)
+     * @param idToken  the parsed {@link IDToken} (claims already extracted)
+     * @param provider the configured OIDC authentication provider (issuer/JWKS source)
+     * @param metadata the per-login OIDC metadata (nonce binding)
+     * @throws Exception if the token is invalid, unsigned, signed by an untrusted key,
+     *                   expired, or fails any claim check
+     */
+    public static void verify(String jwt, IDToken idToken,
+                              AbstractOIDCAuthenticationProvider provider,
+                              OIDCMetadata metadata) throws Exception {
+        if (jwt == null) {
+            throw new SecurityException("ID token is null");
+        }
+        String[] parts = jwt.split("\\.");
+        if (parts.length != 3) {
+            throw new SecurityException("ID token does not contain the expected 3 JWT parts");
+        }
+
+        // --- Header: algorithm and key id -------------------------------------
+        JsonObject header = parseJson(new String(b64Decode(parts[0]), StandardCharsets.UTF_8));
+        String alg = getAsString(header, "alg");
+        if (alg == null || alg.isEmpty()) {
+            throw new SecurityException("ID token header is missing the 'alg' claim");
+        }
+        // Explicitly reject unsigned tokens. This is the core fix for the alg:none
+        // forgery used by CVE-2026-48558.
+        if ("none".equalsIgnoreCase(alg)) {
+            evidence("REJECTED: ID token alg is 'none' (unsigned/forged token)");
+            throw new SecurityException("ID token 'alg' is 'none': unsigned tokens are not accepted");
+        }
+        if (!SUPPORTED_ALGS.contains(alg)) {
+            throw new SecurityException("ID token 'alg' " + alg + " is not a supported signing algorithm");
+        }
+        // Require a non-empty signature segment. Even a valid alg must be backed by a
+        // real signature.
+        if (parts[2] == null || parts[2].isEmpty()) {
+            throw new SecurityException("ID token is missing its signature segment");
+        }
+
+        String kid = getAsString(header, "kid");
+        evidence("VERIFY_START alg=" + alg + " kid=" + kid);
+
+        // --- Resolve the issuer/JWKS metadata from the *configured* provider -----
+        // The issuer and signing keys must come from server configuration, never from
+        // the token itself, otherwise an attacker who controls the token also controls
+        // the key source.
+        IssuerInfo issuer = resolveIssuer(provider);
+
+        // --- Standard claim validation -----------------------------------------
+        String tokenIssuer = idToken.getIssuer();
+        if (tokenIssuer == null || !tokenIssuer.equals(issuer.issuer)) {
+            throw new SecurityException("ID token 'iss' does not match the configured issuer");
+        }
+        String expectedAudience = provider.getClientID();
+        String tokenAudience = idToken.getAudience();
+        if (expectedAudience == null || expectedAudience.isEmpty()) {
+            throw new SecurityException("OIDC provider has no configured client id; cannot validate audience");
+        }
+        if (tokenAudience == null || !audienceContains(tokenAudience, expectedAudience)) {
+            throw new SecurityException("ID token 'aud' does not include the configured client id");
+        }
+        long now = System.currentTimeMillis() / 1000L;
+        if (idToken.hasExpirationTime() && idToken.getExpirationTime() <= now) {
+            throw new SecurityException("ID token has expired");
+        }
+        if (idToken.hasIssuedAtTime() && idToken.getIssuedAtTime() > now + 300L) {
+            throw new SecurityException("ID token 'iat' is too far in the future");
+        }
+        // Nonce binding when the provider issued a nonce (e.g. Azure implicit flow).
+        if (metadata != null && metadata.nonce != null) {
+            String tokenNonce = idToken.getNonce();
+            if (tokenNonce == null || !tokenNonce.equals(metadata.nonce)) {
+                throw new SecurityException("ID token 'nonce' does not match the issued nonce");
+            }
+        }
+
+        // --- Signature verification --------------------------------------------
+        JsonObject jwks = fetchJwks(issuer.jwksUri);
+        PublicKey key = selectKey(jwks, kid, alg);
+        if (key == null) {
+            throw new SecurityException("No JWKS key matched the ID token 'kid'/'alg'");
+        }
+        byte[] signedContent = (parts[0] + "." + parts[1]).getBytes(StandardCharsets.UTF_8);
+        byte[] signature = b64Decode(parts[2]);
+        if (alg.startsWith("ES")) {
+            signature = ecdsaConcatToDer(signature);
+        }
+        String jcaAlg = jcaAlgorithmFor(alg);
+        Signature verifier = Signature.getInstance(jcaAlg);
+        verifier.initVerify(key);
+        verifier.update(signedContent);
+        if (!verifier.verify(signature)) {
+            evidence("REJECTED: ID token signature verification failed");
+            throw new SecurityException("ID token signature verification failed");
+        }
+        evidence("VERIFY_OK: ID token signature and claims verified");
+    }
+
+    // -------------------------------------------------------------------------
+
+    private static final class IssuerInfo {
+        final String issuer;
+        final String jwksUri;
+        IssuerInfo(String issuer, String jwksUri) {
+            this.issuer = issuer;
+            this.jwksUri = jwksUri;
+        }
+    }
+
+    private static IssuerInfo resolveIssuer(AbstractOIDCAuthenticationProvider provider) throws Exception {
+        if (provider instanceof OIDCAuthenticationProvider) {
+            OIDCAuthenticationProvider oidc = (OIDCAuthenticationProvider) provider;
+            String discovery = oidc.getDiscoveryURL();
+            if (discovery == null || discovery.isEmpty()) {
+                throw new SecurityException("OIDC provider has no discovery URL; cannot resolve issuer/JWKS");
+            }
+            JsonObject wellKnown = parseJson(fetchString(discovery));
+            String issuer = getAsString(wellKnown, "issuer");
+            String jwksUri = getAsString(wellKnown, "jwks_uri");
+            if (issuer == null || jwksUri == null) {
+                throw new SecurityException("OIDC discovery document is missing 'issuer' or 'jwks_uri'");
+            }
+            return new IssuerInfo(issuer, jwksUri);
+        }
+        if (provider instanceof AzureAuthenticationProvider) {
+            AzureAuthenticationProvider azure = (AzureAuthenticationProvider) provider;
+            String tenant = azure.getTenantID();
+            if (tenant == null || tenant.isEmpty()) {
+                tenant = "common";
+            }
+            String issuer = "https://login.microsoftonline.com/" + tenant + "/v2.0";
+            String jwksUri = "https://login.microsoftonline.com/" + tenant + "/discovery/v2.0/keys";
+            return new IssuerInfo(issuer, jwksUri);
+        }
+        throw new SecurityException("Unknown OIDC provider type; cannot resolve issuer/JWKS");
+    }
+
+    private static JsonObject fetchJwks(String jwksUri) throws Exception {
+        synchronized (JWKS_CACHE) {
+            CachedJwks cached = JWKS_CACHE.get(jwksUri);
+            if (cached != null && System.currentTimeMillis() - cached.fetchedAt < JWKS_TTL_MS) {
+                return cached.json;
+            }
+            JsonObject json = parseJson(fetchString(jwksUri));
+            JWKS_CACHE.put(jwksUri, new CachedJwks(json));
+            return json;
+        }
+    }
+
+    private static PublicKey selectKey(JsonObject jwks, String kid, String alg) throws Exception {
+        if (jwks == null || !jwks.has("keys")) {
+            return null;
+        }
+        JsonArray keys = jwks.getAsJsonArray("keys");
+        for (JsonElement el : keys) {
+            if (!el.isJsonObject()) continue;
+            JsonObject k = el.getAsJsonObject();
+            String kKid = getAsString(k, "kid");
+            String kKty = getAsString(k, "kty");
+            String kAlg = getAsString(k, "alg");
+            if (kid != null && !kid.equals(kKid)) continue;
+            if (kAlg != null && !kAlg.equals(alg)) continue;
+            if (kKty == null) continue;
+            if ("RSA".equals(kKty) && (alg.startsWith("RS") || alg.startsWith("PS"))) {
+                return rsaKey(k);
+            }
+            if ("EC".equals(kKty) && alg.startsWith("ES")) {
+                return ecKey(k, alg);
+            }
+        }
+        return null;
+    }
+
+    private static PublicKey rsaKey(JsonObject k) throws Exception {
+        BigInteger n = new BigInteger(1, b64Decode(getRequiredString(k, "n")));
+        BigInteger e = new BigInteger(1, b64Decode(getRequiredString(k, "e")));
+        return KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(n, e));
+    }
+
+    private static PublicKey ecKey(JsonObject k, String alg) throws Exception {
+        BigInteger x = new BigInteger(1, b64Decode(getRequiredString(k, "x")));
+        BigInteger y = new BigInteger(1, b64Decode(getRequiredString(k, "y")));
+        ECParameterSpec spec = ecParamsFor(alg);
+        ECPoint point = new ECPoint(x, y);
+        return KeyFactory.getInstance("EC").generatePublic(new ECPublicKeySpec(point, spec));
+    }
+
+    private static ECParameterSpec ecParamsFor(String alg) {
+        BigInteger p, a = BigInteger.valueOf(-3L), b, gx, gy;
+        String name;
+        switch (alg) {
+            case "ES256":
+                p = new BigInteger("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", 16);
+                b = new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16);
+                gx = new BigInteger("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", 16);
+                gy = new BigInteger("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", 16);
+                name = "secp256r1";
+                break;
+            case "ES384":
+                p = new BigInteger("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff", 16);
+                b = new BigInteger("b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef", 16);
+                gx = new BigInteger("aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7", 16);
+                gy = new BigInteger("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f", 16);
+                name = "secp384r1";
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported EC algorithm " + alg);
+        }
+        EllipticCurve curve = new EllipticCurve(new ECFieldFp(p), a, b);
+        return new ECParameterSpec(curve, new java.security.spec.ECPoint(gx, gy), BigInteger.valueOf(1L), 1);
+    }
+
+    private static String jcaAlgorithmFor(String alg) {
+        switch (alg) {
+            case "RS256": return "SHA256withRSA";
+            case "RS384": return "SHA384withRSA";
+            case "RS512": return "SHA512withRSA";
+            case "PS256": return "SHA256withRSAandMGF1";
+            case "PS384": return "SHA384withRSAandMGF1";
+            case "PS512": return "SHA512withRSAandMGF1";
+            case "ES256": return "SHA256withECDSA";
+            case "ES384": return "SHA384withECDSA";
+            case "ES512": return "SHA512withECDSA";
+            default: throw new IllegalArgumentException("Unsupported algorithm " + alg);
+        }
+    }
+
+    private static byte[] ecdsaConcatToDer(byte[] concat) {
+        if (concat == null) return new byte[0];
+        int len = concat.length / 2;
+        byte[] r = trim(concat, 0, len);
+        byte[] s = trim(concat, len, len);
+        try {
+            java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
+            out.write(0x30); // SEQUENCE
+            int bodyLen = r.length + s.length + 4;
+            if (bodyLen > 0x7f) {
+                out.write(0x81);
+                out.write(bodyLen);
+            } else {
+                out.write(bodyLen);
+            }
+            out.write(0x02); out.write(r.length); out.write(r); // INTEGER r
+            out.write(0x02); out.write(s.length); out.write(s); // INTEGER s
+            return out.toByteArray();
+        } catch (Exception e) {
+            return concat;
+        }
+    }
+
+    private static byte[] trim(byte[] data, int off, int len) {
+        int i = 0;
+        while (i < len - 1 && data[off + i] == 0) i++;
+        byte[] out = new byte[len - i];
+        System.arraycopy(data, off + i, out, 0, out.length);
+        return out;
+    }
+
+    private static boolean audienceContains(String aud, String expected) {
+        if (aud.equals(expected)) return true;
+        // aud may be a JSON array stringified by IDToken; check both raw and bracketed forms.
+        if (aud.startsWith("[")) {
+            try {
+                JsonArray arr = JsonParser.parseString(aud).getAsJsonArray();
+                for (JsonElement e : arr) {
+                    if (e.isJsonPrimitive() && e.getAsString().equals(expected)) return true;
+                }
+            } catch (Exception ignored) {
+            }
+        }
+        return false;
+    }
+
+    // --- JSON / base64 / HTTP helpers ----------------------------------------
+
+    private static JsonObject parseJson(String json) {
+        return JsonParser.parseString(json).getAsJsonObject();
+    }
+
+    private static byte[] b64Decode(String s) {
+        return Base64.getUrlDecoder().decode(s);
+    }
+
+    private static String getAsString(JsonObject o, String key) {
+        if (o == null || !o.has(key)) return null;
+        JsonElement el = o.get(key);
+        if (el == null || el.isJsonNull()) return null;
+        return el.getAsString();
+    }
+
+    private static String getRequiredString(JsonObject o, String key) {
+        String v = getAsString(o, key);
+        if (v == null) throw new SecurityException("Missing required JWK field: " + key);
+        return v;
+    }
+
+    private static String fetchString(String url) throws Exception {
+        try (InputStream in = new URL(url).openStream();
+             ByteArrayOutputStream bout = new ByteArrayOutputStream()) {
+            byte[] buf = new byte[8192];
+            int n;
+            while ((n = in.read(buf)) > 0) bout.write(buf, 0, n);
+            return bout.toString(StandardCharsets.UTF_8.name());
+        }
+    }
+}
