Nov 10, 2026·11 min read·1 visit
Pocket ID versions 2.2.0 to 2.11.0 bind email verification tokens only to a user ID rather than the specific email address being verified, and fail to revoke tokens when a user changes their email. An authenticated attacker can request a token for their own email, change their account email to a victim's address, and redeem the token to forge a verified email claim (email_verified = true), leading to account takeover on connected OIDC client applications.
CVE-2026-115770 is a critical vulnerability (CVSS 9.6) in Pocket ID, an OpenID Connect (OIDC) provider that uses passkey authentication. In versions 2.2.0 through 2.11.0, email verification tokens are bound exclusively to the user account identifier (user_id) rather than the specific email address for which the token was issued. In addition, updating the account email address does not invalidate existing verification tokens. An authenticated user can request a verification token for an attacker-controlled address, modify their profile email to an arbitrary unverified target address, and redeem the previously issued token. This marks the target email address as verified (email_verified = true), enabling account takeover on downstream OIDC relying parties that link or provision accounts based on verified email claims.
Pocket ID is an OpenID Connect (OIDC) identity provider designed to facilitate passwordless authentication via WebAuthn passkeys for connected relying party applications. As an identity provider, Pocket ID acts as the central authority for user identity attributes, issuing signed ID tokens and responding to UserInfo requests with claims such as the unique subject identifier (sub), the user's email address (email), and a boolean flag indicating whether the email has been confirmed (email_verified). Downstream client applications rely on the cryptographic integrity and logical accuracy of these claims to make authorization and account-linking decisions.
CVE-2026-115770 (tracked also as GHSA-rm8c-2cv8-jwf6) is an authentication and state verification flaw classified under CWE-345 (Insufficient Verification of Data Authenticity). Introduced in version 2.2.0 via commit 2c64bebf6a6c88b618c6c3569e3b16196fcb (specifically 2c64bebf6a6c88b618c6c3569e3b26c5fcaf2d8b), the vulnerability affects the self-service email verification workflow handled by UserService in backend/internal/service/user_service.go. The attack surface is exposed through the standard authenticated user REST API endpoints: POST /api/users/me/send-email-verification, PUT /api/users/me, and POST /api/users/me/verify-email.
In a standard Pocket ID deployment, two configuration parameters govern self-service email lifecycle management: allowOwnAccountEdit (which defaults to true, permitting users to modify their own profile attributes) and emailsVerified (which defaults to false, requiring explicit token verification before asserting email_verified: true). When both settings remain in their default states, any authenticated user with low privileges can manipulate the sequence of email verification and profile update operations.
By exploiting a state desynchronization between token issuance and token redemption, an attacker can validate ownership of an email address they control and transfer that validation status to an arbitrary email address they do not control. The only constraint is that the target email address must not already be assigned to another existing user record within the same Pocket ID database. Once the target email is falsely marked as verified, the attacker can authenticate via OIDC to downstream relying parties, compromising accounts that map identities by verified email address.
The root cause of CVE-2026-115770 stems from three compounding architectural defects in how Pocket ID modeled, persisted, and validated email verification tokens prior to version 2.12.0. A secure email verification implementation must establish a cryptographic or stateful binding between three distinct entities: the principal (user_id), the specific attribute value being verified (email), and the secret nonce (token). If any of these bindings are omitted or allowed to mutate independently without invalidating the state, the verification guarantee is broken.
The first flaw was located in the data model definition for EmailVerificationToken inside backend/internal/model/email_verification_token.go. The database schema stored the Token string, the ExpiresAt timestamp, and the foreign key UserID, but omitted a column or field to record the target email address that was active on the user account at the moment of token generation. Consequently, when UserService.SendEmailVerification dispatched an email containing the verification link, the persistent state recorded only that the user identified by UserID had a pending verification request, without recording which address received the secret token.
The second flaw existed in the profile update logic within UserService.UpdateUser, triggered via PUT /api/users/me. When an authenticated user submitted a modified email address, the service updated the email column on the users table and reset email_verified to false. However, the transaction failed to execute a revocation query against the email_verification_tokens table for that user_id. Any previously issued, unexpired verification tokens remained valid in the database across arbitrary modifications to the user's email address.
The third flaw occurred during token redemption in UserService.VerifyEmail, triggered via POST /api/users/me/verify-email. The verification query validated the submitted token by checking only three conditions: token = ? AND user_id = ? AND expires_at > ?. Upon finding a matching record, the service fetched the corresponding User model and unconditionally set user.EmailVerified = true. Because the redemption logic neither verified that the current user.Email matched the address to which the token was delivered nor purged tokens upon email modification, the verification state was applied directly to whatever email string currently occupied the user record.
Examining the vulnerable implementation prior to version 2.12.0 illustrates how the data model and service methods interacted to permit the bypass. In backend/internal/model/email_verification_token.go, the ORM struct lacked an email field:
// Vulnerable model definition prior to v2.12.0
type EmailVerificationToken struct {
Base
Token string
ExpiresAt datatype.DateTime
UserID string
User User
}In backend/internal/service/user_service.go, the SendEmailVerification and VerifyEmail functions relied on this incomplete model:
// Vulnerable token creation in UserService.SendEmailVerification
randomToken, err := utils.GenerateRandomAlphanumericString(32)
expiration := time.Now().Add(24 * time.Hour)
emailVerificationToken := &model.EmailVerificationToken{
UserID: user.ID, // Bound only to UserID; user.Email is not recorded
Token: randomToken,
ExpiresAt: datatype.DateTime(expiration),
}
err = s.db.WithContext(ctx).Create(emailVerificationToken).Error
// Vulnerable token verification in UserService.VerifyEmail
var emailVerificationToken model.EmailVerificationToken
err := tx.WithContext(ctx).Where(
"token = ? AND user_id = ? AND expires_at > ?",
token, userID, datatype.DateTime(time.Now()),
).First(&emailVerificationToken).Error
// Fetches the user record which now holds the modified email address
user, err := s.getUserInternal(ctx, emailVerificationToken.UserID, tx)
// Marks the current (modified) email as verified
user.EmailVerified = true
user.UpdatedAt = new(datatype.DateTime(time.Now()))
err = tx.WithContext(ctx).Save(&user).ErrorIn version 2.12.0 (commit 43aaccd5bfacb76283b11ce039fb3f0716196fcb, PR #1625), the maintainers remediated the vulnerability by removing the legacy email_verification_tokens database table and refactoring the workflow into a dedicated package (backend/internal/emailverification) backed by a Francis actor (EmailVerification). The actor state explicitly binds the hashed token to the exact email address present at issuance:
// Patched state structure in backend/internal/emailverification
type State struct {
TokenHash string
Email string // Explicitly records the email address being verified
ExpiresAt time.Time
}Furthermore, the patched Verify() method enforces an atomic conditional update against the users table. The SQL WHERE clause requires both the id and the email column on the user record to match the values stored inside the verification actor state:
// Patched verification logic in v2.12.0
update := s.db.
WithContext(ctx).
Model(&model.User{}).
Where("id = ? AND email = ?", userID, result.State.Email). // Dual binding check
Updates(map[string]any{
"email_verified": true,
"updated_at": new(datatype.DateTime(time.Now())),
})
if update.RowsAffected != 1 {
return &common.InvalidEmailVerificationTokenError{}
}> [!NOTE]
> Fix Completeness Assessment: The remediation in commit 43aaccd5bfacb76283b11ce039fb3f0716196fcb is complete and eliminates both the state desynchronization and potential race conditions. By hashing the token at rest (TokenHash) and executing a single atomic UPDATE ... WHERE id = ? AND email = ? statement while Checking RowsAffected == 1, any modification to the user's email address between token generation and redemption causes the database update to match zero rows, cleanly rejecting the verification attempt with InvalidEmailVerificationTokenError.
Exploiting CVE-2026-115770 requires low privileges (PR:L), meaning the attacker must possess a valid, authenticated user session on the target Pocket ID instance. The instance must operate with allowOwnAccountEdit = true and emailsVerified = false (both default settings), and the targeted victim's email address must not currently exist in the Pocket ID users table. This scenario commonly occurs in federated or multi-service environments where a victim holds an existing account on a downstream application (e.g., GitLab, Grafana, or Nextcloud), or where an organization pre-provisions downstream permissions by email before the victim has registered their passkey on Pocket ID.
In the first phase of exploitation, the attacker logs into their Pocket ID account configured with an email address under their control (e.g., attacker@attacker-domain.tld). The attacker initiates a verification request by sending an HTTP POST request to /api/users/me/send-email-verification. Pocket ID generates the 32-character alphanumeric token, persists it in email_verification_tokens linked to the attacker's user_id, and dispatches an email containing the ?token=<TOKEN> parameter to attacker@attacker-domain.tld.
In the second phase, the attacker retrieves the verification token from their inbox without redeeming it. Instead, the attacker sends an authenticated HTTP PUT request to /api/users/me to modify their profile's email address to the target victim's address:
PUT /api/users/me HTTP/1.1
Host: pocket-id.example.com
Content-Type: application/json
Cookie: access_token=<ATTACKER_SESSION_JWT>
{
"firstName": "Attacker",
"lastName": "User",
"username": "attacker",
"email": "victim@corporate-target.com"
}Pocket ID processes this update, overwrites the email field on the attacker's user record with victim@corporate-target.com, and sets email_verified to false. Because the update routine lacks token revocation logic, the token generated in phase one remains active in the database.
In the final phase, the attacker submits the token obtained from their own email inbox to the verification endpoint:
POST /api/users/me/verify-email HTTP/1.1
Host: pocket-id.example.com
Content-Type: application/json
Cookie: access_token=<ATTACKER_SESSION_JWT>
{
"token": "<32_CHAR_TOKEN_FROM_STEP_1>"
}Pocket ID validates that the token matches the attacker's user_id and has not expired. It then updates the attacker's user record, setting email_verified = true for victim@corporate-target.com. When the attacker subsequently initiates an OIDC login flow against any connected relying party, Pocket ID issues a signed ID token and UserInfo payload asserting "email": "victim@corporate-target.com" and "email_verified": true.
CVE-2026-115770 carries a CVSS v3.1 base score of 9.6 (Critical) with the vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N. The critical severity rating is primarily driven by the Changed Scope (S:C) metric. While the vulnerability resides within the Pocket ID identity provider (the vulnerable component), the primary security impact manifests across downstream OIDC relying parties (the impacted components) that trust Pocket ID's identity assertions.
When a downstream application integrates with an OIDC provider, it must map incoming federated assertions to local user accounts. Many enterprise and self-hosted applications—such as GitLab, Grafana, Nextcloud, Outline, and custom internal administrative portals—support automatic account linking or Just-In-Time (JIT) user provisioning based on the email claim, provided the identity provider asserts email_verified: true. By forging a verified email claim for victim@corporate-target.com, an attacker achieves full account takeover of the victim's existing profile on the downstream service, resulting in high confidentiality (C:H) and integrity (I:H) impacts.
Furthermore, if a downstream application implements domain-based access control (for example, granting administrative or internal workspace access to any user presenting a verified @corporate-target.com email address), an external attacker who registers a low-privileged account on a publicly accessible or shared Pocket ID instance can escalate privileges into restricted corporate environments without owning a corporate mailbox.
Conversely, relying party applications that adhere strictly to OIDC specification best practices by binding local accounts exclusively to the immutable tuple of Issuer (iss) and Subject Identifier (sub) are not susceptible to account linking takeover via this flaw. Because the attacker's sub claim remains bound to their own unique UUID within Pocket ID, applications that ignore the email claim for authentication identity mapping will treat the attacker as a distinct user, though display-level spoofing of the victim's email address in audit logs or UI profiles will still occur.
Administrators operating affected versions of Pocket ID (>= 2.2.0, < 2.12.0) should upgrade immediately to Pocket ID version 2.12.0 or later. Version 2.12.0 incorporates commit 43aaccd5bfacb76283b11ce039fb3f0716196fcb, which drops the vulnerable email_verification_tokens table, migrates verification state management to an actor-based module, stores the SHA hash of verification tokens, and enforces dual matching of both user_id and email during the database update transaction.
If an immediate upgrade to version 2.12.0 cannot be performed due to operational constraints, administrators can neutralize the exploitation vector within Pocket ID by disabling self-service profile modifications. Setting the configuration option allowOwnAccountEdit = false (or via the corresponding environment variable / admin UI setting) prevents standard authenticated users from invoking PUT /api/users/me to alter their email address after requesting a verification token.
In addition to patching or reconfiguring Pocket ID, security teams should audit the database and logs of instances that were exposed prior to upgrading. Administrators should query the users table for accounts where the email domain does not align with expected user populations or where updated_at timestamps show rapid changes preceding OIDC authentication events. If compromise is suspected, administrators should manually reset email_verified = false for unverified accounts and revoke active OIDC sessions.
Finally, as a defense-in-depth measure, security engineers should review the OIDC client configurations of all downstream relying parties connected to Pocket ID. Where supported by the client software, automatic account linking based on the mutable email claim should be disabled in favor of strict identity matching on the immutable OIDC sub claim.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Pocket ID pocket-id | >= 2.2.0, < 2.12.0 | 2.12.0 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-115770 (GHSA-rm8c-2cv8-jwf6) |
| Vulnerability Class | CWE-345: Insufficient Verification of Data Authenticity |
| CVSS v3.1 Score | 9.6 Critical (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N) |
| EPSS Score | 0.17% (5.94th percentile) |
| Affected Versions | Pocket ID >= 2.2.0, < 2.12.0 |
| Patched Version | Pocket ID 2.12.0 |
| Primary Impact | Arbitrary email verification bypass leading to downstream OIDC account takeover |
| CISA KEV Status | Not Listed |
The software does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data or bind verification state to an unintended context.
A Denial of Service (DoS) vulnerability exists in the web display module (urwid/display/web.py) of the Urwid console user interface library for Python in versions prior to 4.0.7. The handle_short_request() function unconditionally encodes HTTP POST input as ASCII without exception handling and opens a FIFO pipe file descriptor without a guaranteed cleanup block. Submitting non-ASCII characters triggers an unhandled UnicodeEncodeError that terminates CGI request processing and leaks the open file descriptor. Additionally, the screen update polling routine decodes Unix domain socket streams iteratively as UTF-8 without exception handling or deterministic socket closure, enabling uncaught UnicodeDecodeError exceptions and socket leaks.
CVE-2026-102895 is an improper authentication and origin validation vulnerability in GPUStack versions prior to 2.2.0. By default, the API authentication dependency in gpustack/api/auth.py grants built-in administrator privileges to unauthenticated requests resolved as originating from 127.0.0.1 when the force_auth_localhost flag is false. Because client IP resolution trusts same-host reverse proxy TCP peers and unvalidated X-GPUStack-Real-IP headers, remote unauthenticated attackers can bypass authentication, obtain full administrative control, extract sensitive credentials, and achieve remote code execution via the /v2/inference-backends/from-yaml endpoint.
CVE-2026-87008 (GHSA-85gg-2gfq-q95m) is a high-severity symbolic link following and path traversal vulnerability in the `code-graph-rag` Python package prior to version 0.0.589. The flaw resides in the `should_skip_path` path validation routine inside `codebase_rag/utils/path_utils.py`, which performs directory containment checks using unresolved lexical paths rather than canonicalized filesystem paths. Consequently, symbolic links committed to an untrusted repository that point to targets outside the project root bypass boundary enforcement. Attackers who supply a crafted repository containing symbolic links can leverage the `AstGrepService` tool (`structural_search` and `structural_replace`) to read or overwrite arbitrary files on the host filesystem within the privileges of the running process.
FileGator versions prior to 7.15.0 contain a missing authorization check (CWE-862) and predictable identifier generation flaw (CWE-330) in the batch download implementation. When a user initiates a batch download, FileGator generates a temporary ZIP archive identified by PHP's non-cryptographic uniqid() function without binding the resulting identifier to the creating user's session. Consequently, any authenticated user with read, download, and batchdownload permissions who obtains or predicts an active archive identifier can retrieve another user's private files via GET /batchdownload, bypassing per-user home directory isolation and deleting the victim's temporary archive upon download completion.
An authorization bypass and insecure direct object reference (IDOR) vulnerability exists in FileGator versions prior to 7.15.0 within the chunked file upload reassembly handler. When processing multipart uploads in backend/Controllers/UploadController.php, the application fails to namespace the target assembly filename with a user-specific prefix inside the shared temporary storage directory. Authenticated users with upload privileges can manipulate chunk metadata parameters to exfiltrate, corrupt, or delete arbitrary transient files belonging to other users or the system.
CVE-2026-77596 is an authorization bypass vulnerability in the Kubeflow Pipelines API server affecting multi-user deployments prior to version 2.18.0. The HTTP multipart upload handlers prematurely terminate authorization checks and return success when the target namespace parameter is empty or omitted. Because the default configuration allows pipelines without namespaces to function as cluster-wide shared resources, any low-privileged tenant can create shared pipelines or inject arbitrary pipeline versions into existing shared pipelines without undergoing Kubernetes SubjectAccessReview validation. When other tenants execute unpinned recurring runs tracking these shared pipelines, the attacker-supplied workload runs inside the victim tenant's namespace using the victim's service account credentials.