OIDC Authentication Bypass: Trusting a Token You Never Verified

OIDC Authentication Bypass: Trusting a Token You Never Verified

In late June 2026 CISA added a new entry to its Known Exploited Vulnerabilities catalog: CVE-2026-48558, a CVSS 10.0 flaw in SimpleHelp, a widely deployed remote monitoring and management tool. It was already being used in the wild to plant infostealers and other malware. The root cause was not a memory bug or a clever injection. It was a plain OIDC authentication bypass: the login code read the identity claims inside a token and trusted them, but it never checked that the token was signed by anyone real. This post pulls that mechanism apart on a safe, invented example so you can spot the same shape of bug in your own app.

What OIDC tokens actually promise

OpenID Connect sits on top of OAuth and gives an application a way to say “this request belongs to this person.” When a user signs in, an identity provider mints an ID token. That token is a JWT, a JSON Web Token, made of three parts joined by dots: a header, a payload, and a signature. The payload carries identity claims like sub (the user id), email, and often groups for role or team membership.

The important word is signed. The identity provider signs the token with a private key. It publishes the matching public keys at a JWKS endpoint, usually found through /.well-known/openid-configuration. A relying party, meaning the app receiving the token, is supposed to fetch those keys and verify the signature before it believes a single claim. The signature is the only thing that separates a token the provider issued from a string of JSON an attacker typed by hand.

If you want the deeper split between proving who someone is and deciding what they may do, we cover it in authentication versus authorization. OIDC lives on the authentication side, and this bug lived there too.

The OIDC authentication bypass, claim by claim

Here is a decoded ID token payload for a made up admin panel we will call Acme Console. Nothing here is secret. The payload is base64url, not encryption, so anyone holding the token can read it:

{
  "iss": "https://id.acme-console.example/",
  "aud": "acme-console",
  "sub": "9f14c2",
  "email": "tech@acme-console.example",
  "groups": ["support-technicians"],
  "exp": 1782000000
}

A correct login flow does two things with this token. First it proves the token is genuine by checking the signature against the provider’s JWKS. Only then does it read the groups claim and decide the session is a support technician. The vulnerable pattern skips straight to the second step. In pseudo code, the flaw looked like this:

const parts = token.split(".");
const payload = JSON.parse(base64UrlDecode(parts[1]));

const session = provisionSession({
  email:  payload.email,
  groups: payload.groups,   // trusted as is
});
// parts[2], the signature, is never checked

Read that again. The code splits the token, decodes the middle part, and builds a session from whatever it finds. The third segment, the signature that is the entire point of a JWT, is never fetched, never compared, never used. Any attacker who knows the token’s expected shape can write their own payload, set groups to the administrator group, attach any garbage after the second dot, and send it. The app reads the claims, sees an admin, and hands over an admin session. No password, no second factor.

The token was treated as an identity document. It was never checked against the seal that makes it one. Reading a claim is not the same as verifying it.

In SimpleHelp’s case the affected path was OIDC login configured with group authenticated login. An unauthenticated attacker could forge a JWT, sail past multi factor authentication, and impersonate any technician account, up to and including privileged administrators. The vulnerability class is catalogued as CWE-347, Improper Verification of Cryptographic Signature. Affected builds were 5.5.15 and prior, plus 6.0 prerelease builds. Internet scans found roughly 14,000 exposed SimpleHelp servers, of which about 1,000 were directly vulnerable. On an RMM tool, an admin session is the keys to every managed endpoint, which is why this became a malware delivery route so quickly.

The cousin bug: trusting the header

There is a related failure worth naming. Even code that does call a verify function can be tricked if it lets the token’s own header pick the algorithm. An attacker sets alg: none to claim the token needs no signature, or swaps a strong asymmetric algorithm for a weak one the server verifies with the wrong key. That is JWT algorithm confusion, and it lands in the same place as a missing check: a forged token accepted as real.

How to spot it in your own app

You do not need the source to test for this. You need three quick observations:

  • Alter the signature and see if login still works. Take a valid token, flip a few characters in the third segment, and present it. A correct app rejects it outright. A vulnerable one logs you in, which proves the signature is decoration.
  • Watch for a JWKS fetch. A relying party that verifies signatures has to fetch the provider’s public keys. If the app never calls the JWKS or discovery endpoint during login, it has no key to verify against, so it cannot be verifying anything.
  • Try alg: none and edited claims. Craft a token with the algorithm set to none and a changed email or groups value. If it is accepted, the app is trusting the payload and, at best, trusting the header too.

How to prevent it

The fix is one habit, applied without exception: verify before you trust. A safe version of the Acme Console flow does the whole check in one call and refuses the token unless everything holds:

const { payload } = await jwtVerify(token, JWKS, {
  issuer:   "https://id.acme-console.example/",
  audience: "acme-console",
});
// throws unless the signature, iss, aud, and exp all check out

Concretely, that means:

  • Verify the signature against the issuer’s JWKS before reading any claim. Fetch the published public keys and confirm the token was signed by the key the provider advertises.
  • Pin the accepted algorithms server side. Decide which algorithms you allow and reject everything else, so a token cannot talk you into none or a downgrade.
  • Validate iss, aud, and exp. A valid signature on a token meant for a different app, or one that expired last year, is still the wrong token. Confirm it was issued by your provider, for your app, and is still in date.
  • Reject unsigned tokens. There is no legitimate reason to accept a JWT with no signature in a login flow. Treat an absent or empty signature as an immediate failure.

Every one of these is standard in mature OIDC libraries. The bugs show up when a team hand rolls the token parsing, or turns verification off during testing and forgets to turn it back on. For more on this family of failures, see our access control writing.

The assumption that broke

The failure was not cryptography. The math was fine and the provider signed everything correctly. The app simply never asked to see the proof. It read a claim that said “administrator” and believed it, the same way it would believe any string it was handed. This is the whole story: a token was trusted for what it said, not for what it could prove. That gap between reading a claim and verifying it is exactly the kind of assumption an autonomous security researcher that tests an application’s beliefs and backs findings with hard evidence is built to catch, before someone else forges the token first. More on how that works on our about page.

Frequently asked questions

What is an OIDC authentication bypass?

It is a login flaw where an app reads the identity claims inside an OIDC token but never verifies the token’s signature. Because the claims are trusted without proof, an attacker can forge a token and be accepted as any user, including an administrator.

Why is the signature the important part of a JWT?

The payload of a JWT is base64url encoded JSON that anyone holding the token can read or rewrite. The signature is the only thing that proves the identity provider issued it. Skip the signature check and the claims prove nothing.

How do you test for it?

Take a valid token, change a few characters in the signature segment, and try to log in. A correct app rejects it outright. If login still works, the signature is not being checked. Presenting a token with the algorithm set to none is a fast second test.

How do you prevent an OIDC authentication bypass?

Verify the signature against the issuer’s published keys before reading any claim, pin the algorithms you accept so a token cannot request none, validate the issuer, audience, and expiry, and reject unsigned tokens.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.