Identity-provider authority boundaries: ZITADEL token exchange, Keycloak stale link proof, Nezha Host-injected redirect (3 GHSAs)¶
Source: hourly offensive-security scan of GitHub Security Advisories on 2026-09-16 (reviewed wave published 2026-09-15/16).
Three identity-boundary records that share one operator lesson: an identity provider's authority decision is only as strong as the weakest binding in the flow — exchanged-token provenance, link-proof lifecycle, or the value the server uses to build the redirect target.
Advisory table¶
| GHSA | CVE | Sev. | Boundary |
|---|---|---|---|
| GHSA-vrh8-c9cm-wh8v | CVE-2026-56668 | high (CVSS 8.1) | ZITADEL OAuth2 Token Exchange (urn:ietf:params:oauth:grant-type:token-exchange): the server never verifies that the submitted access token belongs to / is authorized for the client initiating the exchange, and does not constrain requested scopes to the original token's scopes. A low-privilege app's token can be exchanged for a token on a highly-privileged target application — administrative project roles, profile data, cross-application access. Public clients (no secret) make it require zero client authentication. Affected: 4.0.0–4.15.2 and 3.0.0–3.4.12, fixed 4.15.3. |
| GHSA-prm3-wvj6-64vx | CVE-2026-92358 | medium (CVSS 6.4) | Keycloak first-broker-login account linking: the temporary cross-browser proof created when a user confirms an account-link request is not cleared after the link is established, nor when the user later removes the link. An attacker who controls the external identity can reuse the leftover proof to silently re-establish the link and access the victim account with no further confirmation. |
| GHSA-rf68-8gjr-36q7 | — | low | Nezha v2.2.3 regresses the GHSA-9rc6-8cjv-rcvx Host-header fix when the new optional dashboard_host setting is left empty: /api/v1/oauth2/{provider} again reflects the request Host header into the OAuth2 redirect_uri sent to the IdP, even with install_host configured. A forged Host at OAuth-start time can point the authorization code at an attacker origin. Configuration-dependent but realistic because the empty default is the upgrade state. |
Why this is worth an operator page¶
- Token exchange is the newest large IdP attack surface and it is consistently under-tested. RFC 8693 exchanges cross application boundaries inside one IdP; if the server skips "does this subject token belong to this client?" and "are requested scopes ⊆ original scopes?", the entire per-app role model collapses to "hold any token." Any ZITADEL/Auth0-style deployment with token exchange enabled deserves a dedicated exchange matrix, not a spot check.
- Proof lifecycle bugs are quiet and persistent. The Keycloak record is not a bypass of the confirmation step — the confirmation worked. The finding is that the receipt outlived its purpose, and the product also fails to revoke it when the user explicitly unlinks. Identity proofs must be single-use and revoked when the relationship they authorized is dissolved. This is a reusable check on any "confirm in another device/browser" flow (SSO link, device pairing, passkey enrollment).
- Fix regressions hide behind new optional config. The Nezha record is a previously-fixed Host-injection that returns whenever a newly added setting defaults to empty. When a product ships a fix that is gated on configuration, the empty/unconfigured state is the first thing to re-test — upgrade paths and defaults frequently land in the regressed state.
Validation workflows (authorized scope only)¶
Disposable IdPs, synthetic clients, synthetic users only
Run every proof against a lab IdP you own with throwaway realms/organizations, fake client credentials, and canary users. Never exchange real user tokens, never target a production IdP, and never collect real authorization codes or tokens in report evidence.
1. ZITADEL-style token-exchange matrix¶
Preconditions: lab IdP with token exchange enabled, one low-privilege client/app (A) you can authenticate as, one high-privilege target app (B) with an administrative role assignable to a canary user, and two canary users (low, high).
- Mint a low-privilege access token for app A (client credentials or a disposable low-role user).
- Send a token-exchange request with
subject_token= A's token,audience= app B, andscoperequesting B's highest available role/scope. - Record the decision table: token accepted/rejected, returned token's app audience, returned roles/scopes, and whether requested scopes exceeding the subject token's scopes are honored.
- Cross-product variants: submit app B's own token from client A (token/client mismatch), submit a token from an unrelated app C, submit with a public client (no secret), and request scopes beyond the subject token's grants.
- Positive: a low-privilege token yields a B-audience token with administrative roles, or out-of-scope requests honored. Confirm the negative on the patched build (4.15.3+), then delete all minted tokens and canary grants.
Report the exchange as a provenance + scope-containment failure with the exact grant type, request parameters (token labels only), and the role/scope delta between subject and issued token.
2. Link-proof lifecycle check (Keycloak-style first-broker-login)¶
Preconditions: lab realm with identity brokering, a fake external IdP you control, two disposable browsers/profiles (victim + attacker), and a canary victim account.
- Drive the normal flow: victim initiates login via the external IdP, confirms account linking from the second browser, capture the temporary proof's lifetime and binding.
- After the link succeeds, replay the proof from the attacker-controlled external identity — the check is whether the still-stored proof lets the attacker's identity re-establish (or re-bind) the link without victim confirmation.
- Add the removal path: victim manually unlinks, then the attacker replays the same proof again. A leftover proof that re-links after explicit removal is the durable finding.
- Record: proof creation trigger, TTL, subject binding, whether link success clears it, whether manual unlink clears it, and the re-link decision. Use a harmless canary attribute (e.g., a marker email on the fake IdP), never real identities.
3. Host-header redirect regression check (Nezha-style)¶
Generalize the check to any self-hosted app that builds an OAuth2 redirect_uri at login-start time:
- Enumerate the app's configuration surface and find settings that changed in recent releases; note every newly-optional setting whose empty/default state selects a different code path.
- Start an OAuth login with a forged
Hostheader (point it at an owned no-content observer) and capture the exactredirect_urisent to the IdP in the outbound request — compare against the configured install/dashboard host. - Repeat with the setting populated (control) and empty (suspected regression). The positive is
redirect_urihost following the request header in the empty/default state on a build that supposedly fixed the original advisory. - Prove only destination control at the IdP request boundary with an owned observer; never complete a real code exchange with a victim authorization code.
Adjacent records processed without publication¶
From the same reviewed wave: @zereight/mcp-gitlab SSRF/DNS-rebinding/safety-control-bypass and Http4s Ember parser records were already published on the Sept 15 MCP/agent local-surface page and the Sept 15 Http4s/Traefik page — the GitLab MCP trio (SSRF, DNS rebinding to local Streamable HTTP transport, execute_graphql read-only/allow-list bypass plus unauthenticated transports) and all 13 Http4s records fold there. ZITADEL GHSA-v859-c572-qh5p (cross-org project-grant role revocation skips roles when multiple roles are deleted at once, fixed 4.16.0) is tracked as an authorization-state hygiene note: re-check residual grants after bulk revocations in any IdP, but it adds no new workflow. GHSA-4595-rvpx-4q34 emp3r0r unauthenticated HTTP-polling session creation before CBOR MsgAuth is a C2-transport pre-auth dispatch DoS — the axis (authentication deferred past session establishment) is already covered on existing listener-trust pages. October CMS GHSA-2xmm-m4wv-3fjh (http prefix check + :// substring admitting phar:///file:// wrappers into the resizer cache, gated on an existing template-authoring primitive) and GHSA-2ff2-mx52-q8wp (widget session base64(serialize()) consumed with unrestricted unserialize(), cms.safe_mode only) extend axes already on the CMS renderer and PHP deserialization pages — scheme-prefix vs scheme-allowlist is a worthwhile reminder when any field later reaches a PHP file operation, but the preconditions keep it below publication. Netmaker GHSA-r8cr-4f9w-7r75 (authenticated boolean-based SQLi via string-concatenated DELETE from a DNS path parameter), ESPHome renamed-auth-env-var auth disable, and emp3r0r/libp2p-quic records were processed without publication.
Reporting heuristics¶
- For token exchange, report the two missing checks separately: token↔client provenance and scope containment. They can be fixed independently.
- For proof-lifecycle bugs, state explicitly which lifecycle event failed to revoke the proof (link success, link removal) — that is the boundary, not "account linking is insecure."
- For config-gated regressions, cite the original advisory being regressed, the exact setting whose empty state restores the vulnerable path, and the version. Empty-default regressions are frequently absent from vendor fix notes.
- Keep evidence to parameter tables and token/proof labels; never include live tokens, authorization codes, or real IdP metadata.
October 4 follow-up: ZITADEL Login V1/V2 account-takeover cluster — the identify-only session as a privileged foothold (10 GHSAs)¶
Source: GitHub advisories published 2026-10-04 15:30Z (fixed 3.4.14/3.4.15 and 4.16.2/4.17.1/4.17.3). Ten legs, four rated critical, all inside the hosted Login UIs. The cluster generalizes this page's core claim: the weakest binding in an IdP flow is usually a state the flow treats as "in progress" but the handlers treat as authenticated.
- Identity binding without a factor or permission (GHSA-98x8-wvr5-pxqj, CVE-2026-105207, 9.8): user↔external-IdP links are created without verifying a primary factor or the caller's permission — including on identify-only Login V2 sessions and via User Service V2
AddIDPLink. Unauthenticated attacker knowing a login name binds their own IdP identity to the victim's account, then signs in as the victim. Direct sibling of the Keycloak stale-link-proof leg above: that one abused a leftover link receipt; this one never required the receipt at all. - Authenticator enrollment on identify-only sessions (GHSA-4727-2r9w-rpch, CVE-2026-105212, 7.5): passkey/authenticator enrollment accepted on identify-only login sessions before any primary factor is verified — knowing a login name is enough to register an attacker authenticator and log in, bypassing password and MFA.
- Second-factor enrollment/initialization handlers act pre-primary-factor (GHSA-j55w-p4wc-frwh, CVE-2026-105210, 8.2): Login V1 TOTP / OTP-SMS / OTP-Email / U2F enrollment handlers act on the identify-only session; attacker-controlled factors enroll, the verified phone number gets overwritten, and discrepant errors yield user enumeration.
- OTP codes returned in server-action responses (GHSA-mq63-7hcv-jfrc, CVE-2026-105211, 8.1 critical): the
returnCodedelivery type echoes OTP-Email/OTP-SMS codes back in the HTTP response — an attacker who knows the login name reads both codes from the server action and walks out with an MFA-authenticated session, including admin accounts. Delivery-channel abstraction where one mode returns the secret to the requester. - Account squatting on unverified external identity (GHSA-639g-x9rp-hw3x, CVE-2026-105215, 9.1): the "external account not found" registration endpoint trusts client-supplied
IDPConfigID+ExternalUserIDwith no completed IdP callback — pre-create an account bound to the victim's external IdP identity; the victim's later genuine IdP login lands in the attacker-seeded account. Reverse-direction account-linking squat: the link is asserted, not proven. - Org header scoped to caller, not target (GHSA-hcch-574x-xrm8, CVE-2026-105209, 9.6 critical): passkey/passwordless enrollment-code issuance checks only the
x-zitadel-orgidheader (the caller's org), not the target user's org — user-write permission in org A buys an enrollment code for a user in org B on the same instance. Same class as the OpenAM session-verb legs: authorize against the target object's scope, never the request envelope's. - Cross-org read authorization keyed on caller's org (GHSA-pmw5-r3fq-5gm7, CVE-2026-105206): User Service verifies
user.readagainst the caller's org instead of the target user's org — org-scoped readers enumerate other orgs' registered authentication-method types viaGET /v2/users/{userId}/authentication_methods. Enumeration oracle feeding the enrollment legs above. - Malleable unauthenticated intent tokens + timing race (GHSA-xv85-7fx2-46j9, CVE-2026-105208, 7.7): IdP intent tokens are encrypted but unauthenticated and malleable; tamper your own token to be accepted for another user's in-flight external-login intent, predict the intent identifier, win the race on
/v2/idp_intentsor/v2/sessions, steal the victim's IdP tokens or hijack the session. Unauthenticated-ciphertext pattern/state handoffs are bearer credentials. - Tenant lifecycle state not rechecked at auth (GHSA-3fc9-77fj-j3c7, CVE-2026-105213, 8.2): Login V2 checks only the individual user's status, never the organization's inactive state — users of a deactivated org keep signing in, minting sessions, and refreshing tokens. Disabled-tenant recheck axis, sibling of the Keycloak disabled-client recheck rule.
- Domain-verification SSRF via bare default client (GHSA-66r9-3738-gxmw, CVE-2026-105214): organization domain HTTP verification fetches the challenge with Go's default
http.Getinstead of the project's protected (SSRF-guarded) client — register a domain that redirects to loopback/internal/metadata to port-scan the internal network. Exact hardened-sibling-grep axis promoted the same day on the Gitea OIDC picture-claim leg: when a repo has a guarded HTTP client, grep forhttp.Get/http.DefaultClientcall sites; each one is a bypass candidate.
Replayable checks (lab IdP only)¶
- Session-state gating matrix. Enumerate every Login UI handler and classify which session state it requires: pre-identify, identify-only, primary-verified, second-factor-verified. Any enroll/link/enrollment-code/phone-update handler reachable at identify-only is a finding — that sweep alone would have caught four of the ten legs.
- Delivery-mode decision table. For every OTP/email/SMS delivery abstraction, request each delivery mode and diff responses; a mode that returns the code to the requester (or its hash/length) is a transport leak independent of channel security.
- Target-scope audit on multi-tenant verbs. For every org-header/org-param-carrying write (enrollment code, role grant, session action), supply a target object ID belonging to a second lab org and check whether authorization consults the target's org; also query the read sibling (
authentication_methods) as the low-priv caller. - Tenant-deactivation probe. Deactivate a lab org, then replay a valid login, an existing session, and a refresh-token exchange; any success is the lifecycle-recheck miss.
- Intent/handoff token tamper test. For any short-lived cross-request token (login intent, pairing, password-reset), flip user/tenant-bound fields in the ciphertext without recomputing and check acceptance for a second user's flow; pair with identifier-predictability measurement.
- Protected-client grep.
grep -R "http.Get\|http.DefaultClient"in any product that ships an SSRF-guarded HTTP client; verify each bare call site against untrusted URLs (domain verification, webhooks, avatars,jwks_uri).
Report shape for this cluster: per-leg, name the missing gate precisely ("enrollment acts on identify-only session", "authz keyed on caller org not target org", "org state not rechecked at mint") — the vendor fixed ten distinct gates in four releases, so consolidated "IdP ATO" framing undersells each independently patchable boundary.
October 5 follow-up: Keycloak registration attributes, pre-validation cert fetch, and session-note token bleed (3 GHSAs)¶
Source: GitHub advisories published 2026-10-05 06:30Z. Three more Keycloak legs, one per flow stage — client registration, certificate authentication, and token issuance — each breaking a different binding in the same "trusted IdP config object" assumption.
- Open-introspection client via unfiltered registration attributes (GHSA-q5jp-7g4g-ch69, CVE-2026-105306, 6.5): Dynamic Client Registration copies attacker-supplied client attributes onto the new client without filtering security-sensitive ones, so a client registered with a valid Initial Access Token can carry an attribute set that bypasses audience checks during token introspection — the registered client then reads roles, identity claims, and session details out of access tokens minted for other applications in the realm. Durable axes: (1) client-attribute round-trip — in any OAuth AS, register a client echoing every writable attribute, then diff which attributes change enforcement behavior at introspection/token endpoints (registration surface and enforcement surface are routinely written by different code); (2) Initial Access Token is a least-privilege credential in threat models, but here one registration with it yields cross-client token reads — enumerate what a lowest-tier registration credential actually unlocks end-to-end, not just "can I register". Probe: register a canary client, request its introspection of another canary client's token, decision-table accepted/rejected + returned claims before/after toggling the attribute.
- Blind SSRF from unvalidated certificate revocation URLs (GHSA-hg66-m76j-q2rm, CVE-2026-105301, 4.0): with X.509 client-certificate authentication configured for CRL Distribution Points or OCSP checking, Keycloak fetches the CRL/OCSP URL embedded in the presented (still-untrusted) certificate before full validation completes — a crafted cert pointing its DP/OCSP at an internal endpoint gets the IdP to make an outbound request on the attacker's behalf during the TLS/login handshake. Rule: any revocation/chain-URL fetch (CRL DP, OCSP responder, and the analogous
jwks_uri/scopeclaim fetches on the OpenAM and July 28 pages) is a pre-trust network sink — the data driving the fetch originates from attacker input, so URL-grpc scheme, host, and port all deserve canary-callback testing on cert-auth-enabled realms. Proof stays blind-callback: owned observer, timing/response-shape diff, never internal-service probing. - Session-note mapper bleeds upstream IdP tokens into downstream tokens (GHSA-3xph-hcf4-hx94, CVE-2026-105302, 5.7): the User Session Note mapper copies any named session note into issued tokens with no sensitive-key filtering — a delegated administrator of one client can add a session-note mapper for the note holding a user's federated bearer token from an external IdP, then harvest upstream Google/Microsoft-style tokens out of tokens issued to their own managed application. Rule: protocol mappers are read primitives over the server's session store; enumerate every mapper type (claims, audience, session note, user metadata) against every session-store key you can name from the broker config, from a lowest-tier delegated-admin account. This is the identity-broker mirror of the Sept 16 IdP page's own thesis: the realm's internal credential bag is reachable through a config surface a delegated admin legitimately controls. Validation: two-client lab realm with a fake upstream IdP minting a canary federated token, add the mapper from a delegated-admin user, decode the issued token, confirm canary presence — never against real upstream providers.
Cross-refs: introspection-audit and disabled-state recheck axes already on the June 11 Keycloak batch page; jwks_uri pre-trust fetch sibling on the June 22 OpenAM page and July 28 SSRF page; future Keycloak legs fold here or on the June 11 page per flow stage (registration/issuance here, admin-REST verb drift there).
October 8 follow-up: Eclipse Ditto Explorer — split-authority OAuth where the config URL IS the authority (GHSA-g8w6-jp67-cwcc / CVE-2026-107503, high, Explorer 3.6.0–3.9.7)¶
A crafted environments link sets an attacker-controlled OIDC authority while autoSso is enabled. The UI then starts a genuine login at the real identity provider — the authorization redirect itself is legitimate — but exchanges the returned authorization code and its PKCE code_verifier at the attacker-controlled token endpoint, letting the attacker redeem the code for the victim's access and refresh tokens. An attacker-controlled api_uri variant sends the victim's bearer token or Basic credentials straight to the attacker. Because the environment config persists, every later visit to the UI without the crafted link repeats the theft.
Durable axes for any SPA/ops-console that takes config from the URL:
- Authority-split OAuth. The classic
redirect_uricheck is the wrong gate here: the IdP is genuine, the client is genuine, only the token-exchange endpoint is attacker-chosen. Audit which OIDC discovery document the SPA uses and where that discovery URL came from — iftoken_endpointcan transit untrusted input (URL param, persisted storage, tenant config), the authorization code + PKCE guarantees nothing; PKCE binds code↔client, not code↔token endpoint. Probe: craft an environment link pointing discovery/token at an owned observer, complete your own login, confirm the observer receives the code + verifier; never test against real users. - Persisted client-side config = durable phishing implant. A one-click link that rewrites where the app sends tokens, surviving across sessions, converts a single phish into standing token theft — worth more on any bounty triage than the open-redirect-class it superficially resembles. Enumerate every URL/storage key that reconfigures auth endpoints; check whether any trust decision (prompt, origin check, allow-list) gates writing them.
autoSso-style auto-login turns config injection into no-click-per-session token loss. Auto-initiated auth flows remove the user step from the chain after the initial link click.
Cross-ref: the July 30 URL-policy page covers guard-coverage parity for fetch targets; this leg is its auth-flow mirror — the fetch target that matters most is the one that receives your code and tokens.
October 8 15:3xZ follow-up: Keycloak read-only-role client-secret read + DCR update silently disabling offline-token revocation (CVE-2026-107604 / GHSA-ch4r-x5r9-rqr3, 4.9 · CVE-2026-107623 / GHSA-qxj8-968q-qm7w, 4.3)¶
Two more Keycloak client-management boundary legs for the IdP page:
- Read-only view-clients role exposes the active primary secret of any confidential client (ch4r, realm-admin scope): via the installation-provider and client-registration endpoints, an account holding only the read clients role retrieves client secrets that should be admin-write-gated — secret read verbs leaking through installation/config-export endpoints while everyone audits the write path. Sweep rule: for every client-management field, enumerate which endpoint families render it (admin REST, registration API, installation/download-format endpoints — OpenShift/OIDC client-config downloads,
keycloak.xml, OIDC client JSON); the export family is the classic un-redacted twin. Proof: disposable realm, synthetic client, compareview-clients-only principal responses across all endpoint families; never touch live client secrets. - DCR update silently drops the backchannel-logout offline-token revocation setting (qxj8): a response-serialization bug omits the setting from GET responses, so a standard client update (read → modify → PUT) round-trips a payload that lacks it ⇒ the setting is silently disabled, and offline tokens stay valid after backchannel logout. Durable operator/verification axis rather than an exploit: GET-then-PUT read-modify-write flows silently strip any field the GET response fails to serialize — a config-drift mechanism that's also a quiet security-control-disable path. Test any API's settings surface with a field-presence parity check (fields in PUT request vs fields in subsequent GET); the missing fields are the ones updates destroy. On the offensive side, where you legitimately control a client's DCR token, this is a revocation-defeat path worth checking post-fix.
October 9 19:1xZ follow-up: Pingvin Share X — automatic OAuth email linking where email_verified is never checked (critical ATO)¶
GHSA-7p7c-6qwr-q8j7 / CVE-2026-108157 (critical, Pingvin Share X 0.19.0–<1.22.0): OAuthService.signUp() auto-links an OAuth/OIDC identity to an existing local account on matching email alone, and GenericOidcProvider never checks the email_verified claim. An attacker registers a victim's unverified email on an enabled attacker-controlled (or self-hosted) OAuth provider, then uses that provider's login flow to sign in as the victim — administrators included — while bypassing TOTP entirely (the local second factor belongs to the password flow, not the federated one).
Durable axes for every product with "social login auto-creates/links by email":
- The email claim is only as trustworthy as the IdP that minted it. The exploit is not a bug in the match logic — it's that ANY enabled provider's email claim is treated as proof of ownership. On a target with admin-configurable or self-hosted OIDC, the attack is: register attacker provider → put victim email on attacker's IdP → login. Recon: enumerate
/api/auth/*provider list,.well-known/openid-configurationreachability rules, and whether non-admins can add IdP configs; the missing gate isemail_verifiedAND provider-tier trust, both. - Federated login silently skips the local MFA enrollment. If account linking succeeds, the victim's TOTP is never offered — the second factor is bound to the credential path, not the account. Add to every federated-login matrix: victim has password+TOTP; attacker authenticates via linked IdP; record whether the local factor is enforced (it must be, at minimum for elevated accounts).
- Unverified-email registration on the attacker side is a prerequisite — verify with a disposable chain only. Lab proof: two lab users + a self-hosted OIDC test provider you control (Keycloak/Authelia lab realm), set the attacker IdP account's email to the victim's, complete the OAuth dance, record which account the session binds to. This fixture is also the general-purpose account-linking regression harness for any product's OAuth surface (pairs with the Sept 16 Keycloak link-proof-reuse leg at the top of this page — same trust boundary, opposite defect: that one never cleared the confirmation proof, this one never asks for confirmation).
Cross-ref: the May 19 apostrophe Host-header/password-reset page and July 27 WordPress identity page track adjacent identity-binding drift; future OAuth auto-link legs fold here.
October 9 21:1xZ follow-up: Vikunja OIDC emailfallback — email-only linking where the email_verified claim doesn't even exist in the parser (ATO)¶
GHSA-xv7q-fvmc-jx96 / CVE-2026-62367 (high, Vikunja 1.0.0–2.3.0, fixed 2.4.0): with the per-provider OIDC emailfallback option enabled, the callback resolves identity on (issuer, subject) first, and on a subject miss falls back to WHERE issuer='local' AND email=? against local accounts — no email_verified check, no xms_edov (Microsoft education-disclosure-opt-out) check, no password. The claims struct has no email_verified field at all and a repo-wide grep for both names returns nothing; the code even carries a comment warning the option is "discouraged for untrusted providers". The prior advisory's fix added a TOTP gate on the fallback path — not an email-verified gate — so users without TOTP remain exposed. Same class as nOAuth/Grafana CVE-2023-3128.
Adds three sweep details to the Pingvin Share leg above:
- "The claim is absent from the parser" is stronger than "the claim is unchecked". Grep the OIDC claims struct/decoder for
email_verifiedbefore testing — if the field can't even be parsed, every provider config with email-linking enabled is exploitable regardless of what the IdP asserts. Also check vendor-specific equivalents (xms_edov) which products routinely omit. - Fix-gate mismatch across advisory generations: when a product fixes an OIDC-linking advisory by adding a second-factor gate rather than the claim-validation gate, the population without that second factor is still exposed. For any product with an OIDC-linking advisory history, re-run the linking matrix with a TOTP-less lab account specifically.
- Config-surface recon:
emailfallbackis a per-provider option — enumerate which providers on a target have it enabled (config endpoints,.well-knownrelayed settings, docs-fingerprinted defaults) before theorizing about attacker-controlled issuers; the option flag is the precondition to confirm.
Proof stays bounded to the lab fixture from the Pingvin section (two lab users + self-hosted test realm; set the attacker IdP account's email to the victim's and record which account the session binds to). Vikunja's own coordinated wave of the same morning is on the Oct 9 Vikunja page.
October 11 follow-up: Unla — OAuth2 server that never authenticates the resource owner, and Rill — dynamic client registration with no consent and a non-expiring token¶
Two OAuth-authorization-server bugs from the Oct 11 15:31Z wave, same page family as ZITADEL/Ditto/Pingvin:
- AmoyLab Unla
<=0.10.0(GHSA-pfxh-9349-4f24 / CVE-2026-108865, high): the OAuth2 server never authenticates a resource owner during the flow. Register a client (DCR), hit/authorize, get a code, exchange at/token— zero credentials, and the issued token unlocks the OAuth2-protected MCP prefixes, proxied upstream APIs, and injected credentials. The whole point of an auth code grant is the login step; when the AS skips it,/authorizeis just a token factory. Operator probe: on any self-hosted OAuth2/MCP-gateway AS, run the anonymous full-flow battery — DCR → authorize → token — with no cookies and no IdP session; a 200 with an access token at any step is the finding. Fingerprint: authorize endpoints that redirect with a code without ever rendering a login screen. Unla fronts LLM API keys ("injected credentials"), so the token is a billable-credential oracle, not just an app session. - Rill
0.77.0-0.90.5(GHSA-r44j-vvw7-323g / CVE-2026-108718, high): the admin OAuth server issues authorization codes to dynamically registered clients with no user consent screen, and the token exchange honors along_lived_access_tokenscope — one lured click on an authorization link yields a non-expiring API token with the victim's full permissions. Two missing gates that AS implementations routinely omit for "internal" servers: consent for untrusted clients, and scope allowlists for DCR-registered clients. Operator probe: DCR on the target AS, then check (a) does authorize render consent for the new client_id? (b) can the confidential-scope list include privileged/long-lived scopes from a registered client? A successful DCR+no-consent+privileged-scope triple is a phishing-grade ATO primitive: the link looks like a legitimate authorize URL from the real issuer.
Rules: (1) treat the authorization server itself as the attack surface, not just its clients — DCR endpoints, consent-gating logic, and scope-allowlist policy are testable unauthenticated/low-priv primitives before any client bug is needed; (2) long_lived / non-expiring scopes turn every token theft from a TTL-bounded session into a standing implant — flag any grant path that yields non-expiring tokens, and check refresh-rotation absence as the sibling property; (3) same AS-battery applies to MCP-gateway products generally (Unla's protected surface was MCP prefixes): anonymous DCR→authorize→token against any MCP auth gateway is now a first-recon step.
October 12 follow-up: Keycloak CIBA — the flow itself is not gated per-client, so a public client needs no secret to phish a login (ATO)¶
GHSA-68jh-rrf2-v9vr / CVE-2026-109082 (medium, Keycloak keycloak-services CIBA implementation): Keycloak never verifies that the requesting client is authorized to use the CIBA flow (Client-Initiated Backchannel Authentication — the "login on behalf of a user" grant where the IdP pushes an approval request to the user's device). Any public client (no secret) can initiate a CIBA auth request; the victim sees a real Keycloak-issued approval prompt and approving it hands the attacker's client full account access. The security requirement CIBA's client authentication is supposed to provide — "only confidential, registered backends can push a login to your phone" — was enforced nowhere: the capability exists realm-wide the moment the feature is on.
Operator takeaways:
- Feature-enabled ≠ per-client-authorized. Same flag-vs-enforcement genealogy as the Nginx UI 2FA leg (UI flag
enabled_2favs login-branch predicate) and the JeecgBoot annotation-absence wave: the gate must exist at the client × flow intersection, not just the feature toggle. On any IdP with advanced grants (CIBA, device code, ROPC, token exchange), enumerate each grant from a public client you register yourself in a lab realm: does the token/auth endpoint reject this client × this grant, or only validate the grant's parameters? - A legitimate-looking approval prompt is the phish. The victim-side artifact is genuine IdP UI, so social-engineering defenses (verify the URL, check for warnings) don't fire. Report-grade proof stays in the lab: register a public client, initiate
backchannel_authenticationwith a synthetic user, record whether the prompt appears and what client identity it displays (an unnamed/obscure client name on a real approval push is the user-side tell worth documenting in the report). - Grep fingerprint: OAuth/OIDC server handlers that check feature availability (
isCibaEnabled) but not client capability (isAllowedFlow/allowed-protocol-mappers-style per-client lists) before executing.
October 12 18:3xZ follow-up: ZITADEL four-leg wave — IdP-signed-elsewhere confusion, forgeable session cookies, pre-factor username mutation, and impersonation-role scope drift (4 GHSAs, fixed 4.19.2 / 4.18.0)¶
- SAML IdP intent mismatch — org-admin → any-user ATO across organizations (GHSA-65fv-6pff-4q47/CVE-2026-109372, critical, 4.x <4.19.2, 3.x–3.4.15): the SAML IdP intent callback verifies the assertion signature against the provider named in the request URL but resolves the user from the login intent without checking the two match. An org admin registers a SAML provider with their own signing key, drives a login intent, and lands as any user linked to a different SAML provider on the instance — including other organizations. Signature verification passed; the identity of the verifier was attacker-chosen. Durable rule: on any multi-IdP broker, the proof-verification authority and the account-resolution namespace must be bound to each other — test by registering your own IdP in a lab org and replaying an assertion against an intent created by a foreign IdP; also fold into the split-authority lineage (Eclipse Ditto, Oct 8) where the config URL was the authority.
- Unsigned session cookies + time-ordered session ids → unauthenticated admin ATO (GHSA-ff7c-8692-c559/CVE-2026-109373, critical, <4.19.2): Login V2 accepts forgeable/empty-token session cookies whose ids are time-ordered; guess ids around a victim's sign-in, supply empty tokens, receive rotated tokens — full takeover including MFA-completed admin sessions. Two rules: session identifiers that encode issuance time are enumerable — check id monotonicity/predictability before token entropy; and a handler that accepts an empty credential to rotate a token is a token-mint endpoint wearing a refresh handler's clothes. Lifecycle probe: for every session-bearing cookie, replay (valid / empty / forged-guess) triples and watch which responses mint new material.
- Pre-factor username mutation = lockout DoS on any user (GHSA-2464-29qv-3mj7/CVE-2026-109371, high, <4.18.0): hosted Login V1's change-username handler acts on the account bound to the login flow before any factor is verified — submit victim's login name, post a new username, rename any active human user including admins. Same "pending session is a zero-capability placeholder" rule as the Grav
update_user-before-2FA leg on the new Oct 12 Grav page — cross-product recurrence this week; the pre-factor capability sweep belongs in every IdP/CMS test matrix. - End-user-impersonation roles satisfy admin impersonation (GHSA-7j9c-5h49-hq7h/CVE-2026-109374, medium, <4.19.2):
IAM_END_USER_IMPERSONATOR/ORG_END_USER_IMPERSONATORholders bypass theadmin.impersonationcheck via OIDC token exchange and sign into relying apps as IAM_OWNER with full roles/claims. Rule for any granular-role IdP: impersonation-capable roles are transitive privilege edges — enumerate token-exchange/impersonation grants from the lowest tier and check whether a scoped impersonation grant satisfies the unscoped check (granted-scope vs supplied-scope genealogy).
October 13 follow-up: oauthlib library layer — JSONP revocation-endpoint JS injection and PKCE-plain timing oracle (2 GHSAs, descriptions published Oct 13)¶
The most-deployed Python OAuth2 server library shipped two reusable boundary patterns in one advisory pair (published late Sept, detailed technical descriptions added 2026-10-13):
- JSONP callback reflected as executable JavaScript on the revocation endpoint (GHSA-hj66-6f7g-4r5v / CVE-2026-49264, medium,
enable_jsonp=True):RevocationEndpointwraps responses as'{callback}({body});'on the error path and'{callback}();'on success, with zero validation thatcallbackis a legal JS identifier — the parameter is merged from query string and POST body, so the attacker controls the entire JavaScript returned from a same-origin OAuth endpoint. The success path is the interesting one: even a valid revocation request echoes the injected expression. Operator probe: on any Python-oauthlib-based AS, requestPOST /oauth/revokewithcallback=alert(1)//(and query-string variant) and check whether the response body starts with your literal string; content-typeapplication/javascript/text/javascriptcompletes the stored-reflect XSS primitive for anything that scripts in the path. Fingerprint: any JSONP-capable OAuth route (revoke, token introspection, userinfo) — JSONP is a legacy-browser-era pattern still enabled on auth endpoints where nobody re-tests it, and the revocation endpoint in particular is habitually skipped in OAuth security reviews because it "just revokes tokens." - PKCE
plainmethod compared with==→ timing oracle on the code_verifier (GHSA-xpv3-w29h-x7cv / CVE-2026-49265, medium):code_challenge_method_plainshort-circuits on first mismatch, so repeated/tokensubmissions with the sameauthorization_codemeasure the common-prefix length of the verifier character by character (advisory PoC shows a statistically clean per-character latency delta). Precondition gate: the AS must acceptmethod=plain— which is itself the bigger finding. Operator probe order: (1) enumeratecode_challenge_methodsupport at/authorize(a server that acceptsplainat all is weak PKCE — RFC 9700 guidance is S256-only); (2) if plain is accepted, the token endpoint becomes a replayable timing oracle — high-volume repeated exchanges with prefix-growing verifiers, measuring latency to bisect each position; practical over LAN/co-tenant, marginal over internet jitter, so scope it to internal/red-team engagements; (3) also run the negative check: does the same short-circuit apply to S256 digest comparison (it shouldn't leak verifier material, but it can leak challenge-prefix matches).
Rules: (1) library-layer advisories fan out to every product built on them — when an AS bug lands in oauthlib/authlib/oauth2-server libs, grep your target's stack (requirements.txt leaks, error-page fingerprints, /.well-known response-header quirks) instead of waiting for product advisories; (2) JSONP-capable auth endpoints are an un-retested legacy surface — include callback= reflection on every OAuth route family in recon, not just search/userinfo endpoints; (3) PKCE is only as strong as its weakest accepted method and its comparison implementation — method enumeration is a five-request check that gates the entire timing class.
October 13 follow-up: OpenStack Keystone — delegated-authentication tokens reach the credential API and read MFA seeds (GHSA-qfjc-4hpf-mgw2 / CVE-2026-90460, high, fixed 29.0.3)¶
Tokens obtained via delegated authentication methods — EC2 credentials, application credentials, OAuth1 access tokens, trusts — were not blocked from create/modify/delete on /v3/credentials, and EC2-derived tokens could additionally read credential blobs, exposing TOTP MFA seeds and other stored secrets; PATCH /v3/credentials also never validated the requested post-update project_id, so any delegated token could move a credential into an unauthorized project.
- Scoped/delegated tokens are a separate authorization population — test what they reach, not what they're named for. Application credentials and EC2-signature tokens exist to scope API work, yet here they passed every gate to the identity store's own secrets vault. Sweep rule for any IdP/cloud control plane with delegated token types (app passwords, API keys, service tokens, trusts, impersonation tokens): enumerate the full admin-adjacent API surface (
/v3/credentials-style collections, MFA seed storage, signing certs, webhook secrets) and replay it with each delegated token type. The expected answer is 403/401 per token class; a 200 anywhere is a standing secret-read finding. - TOTP seeds stored as readable credential blobs turn a scoping bug into an MFA-cloning primitive — seed disclosure defeats the second factor offline, forever, without touching the victim. On authorized cloud assessments, treat any credential-read leg as MFA-integrity scope: prove with a synthetic TOTP credential enrolled by a lab user and read it back with a delegated token; never touch real users' seeds.
- PATCH-with-client-supplied destination field (post-update
project_idunvalidated) is the same move-check-the-verb misses as the Payload move-verbs leg: for every update endpoint that can relocate an object between tenants/projects/collections, request a denied destination and compare stored state, in a disposable project.