ZeroHour

CVE-2026-59185

Cross-tenant IDOR in identrail GitHub App connect exposes victim private repos

CVSS 3.1
8.5 high
EPSS
Published
()
Modified
AI analysis

identrail's GitHub App connection-completion endpoint (POST /v1/workspaces/:workspace_id/projects/:project_id/github/connect/complete) accepts a fully client-supplied installation_id and binds it to the caller's workspace without verifying that the installation belongs to the workspace that initiated the connect flow, a cross-tenant IDOR (CWE-639) with missing authorization (CWE-862). An authenticated tenant starts a connect flow to obtain a valid state token, then submits that state together with another customer's GitHub App installation_id (these values are not secret and are enumerable integers), which the service accepts because it only checks that state matches the caller's scope and that installation_id is positive. identrail then mints a GitHub installation access token for the supplied installation_id using its own App JWT, giving the attacker read access to the victim organization's private repositories and persisting the victim's installation as the attacker's workspace connection; the code's rigorous state-token binding (tenant, workspace, project) confirms the missing installation_id ownership check was an oversight, and the feature-gated V2 connector (CompleteGitHubConnector, default off) shares the same gap with an even weaker state-only match. Any tenant on the default-on V1 path is exposed to other tenants' installations, i.e., effectively all identrail workspaces with the GitHub connect feature. No public proof-of-concept, CISA KEV listing, or known in-the-wild exploitation has been reported.

What to do: Request a fixed release from identrail and confirm the fix binds installation_id to the initiating workspace/project (mirroring the existing state-token binding) and covers both the default-on connect/complete endpoint and the flag-gated V2 connector. In the meantime, verify that the installation_id stored on your workspace's GitHub connection is your own, and review your GitHub App installation's audit logs for installation access tokens minted via identrail's App outside your own connect flows, which would indicate cross-tenant access to your private repositories.

Affected
identrail GitHub App connect feature (default-on CompleteGitHubConnection endpoint; also the flag-gated V2 CompleteGitHu
Estimated exposure
unknown (all identrail workspaces using the default-on GitHub connect path are potentially exposed; tenant/workspace count unpublished) — No public active-install, deployment, or customer-count data for identrail is available in the source data, so the number of affected tenants cannot be estimated beyond 'all workspaces on the affected service'.

Order-of-magnitude estimate by the model from install counts, market share and public scan data it knows; verify before quoting.

Description

Identrail Cross-tenant IDOR: Client-supplied GitHub App installation_id is bound to the caller's workspace without ownership verification ## Summary identrail's GitHub App connection-completion endpoint binds a fully client-supplied `installation_id` to the caller's workspace without verifying that the installation belongs to, or was installed by, the workspace that initiated the connect flow. identrail then mints a GitHub App installation access token for the supplied `installation_id` using the app's own JWT, so an authenticated tenant can link any other identrail customer's GitHub App installation into their own workspace and read that victim organization's private repositories. ## Affected code (default-on path, no feature flag) - Route: `internal/api/router.go:3590` — `POST /v1/workspaces/:workspace_id/projects/:project_id/github/connect/complete`. `installation_id` is read from the JSON body or the attacker-controlled `X-GitHub-Installation-ID` header (`router.go:3582-3588`). - Service: `internal/api/github_connect.go:781` `CompleteGitHubConnection`. The `state` token IS rigorously bound to the caller's scope (`github_connect.go:818`: `if stateRecord.TenantID != scope.TenantID || stateRecord.WorkspaceID != project.WorkspaceID || stateRecord.ProjectID != project.ProjectID { ... }`), but the only check on `installation_id` is `request.InstallationID <= 0` (`:794`). The raw client value is persisted as the workspace's connection (`:840`). - Cross-tenant read primitive: `internal/connectors/github/repositories.go:39` `ListInstallationRepositories` → `internal/connectors/github/app.go:170` mints a token via `POST /app/installations/{installationId}/access_tokens` signed with the App JWT, succeeding for any installation where identrail's app is installed. The feature-flagged V2 path `CompleteGitHubConnector` (`github_connect.go:447`, default off) shares the gap and is additionally weaker (matches pending connector by `state` value alone with no caller-scope re-check); a single fix should cover both. ## Intent proof The code binds `state` to `{TenantID, WorkspaceID, ProjectID}` and re-verifies it at completion (`:818`), demonstrating it understands binding is required. The asymmetry — `state` bound, `installation_id` unbound — is the missed check. ## Exploitation 1. Attacker is any authenticated identrail tenant; they call `StartGitHubConnection` for their own workspace and receive a `state`. 2. Attacker calls the completion route with `{state: <their state>, installation_id: V}` where V is a victim org's identrail GitHub App installation id (installation ids are not secret — they appear in post-install redirect URLs, webhook payloads, the org's GitHub App settings, and are enumerable integers). 3. `CompleteGitHubConnection` accepts (state matches attacker scope), `ListInstallationRepositories(V)` mints a token for V and lists the victim org's private repos, and the connection is persisted under the attacker's workspace. 4. Attacker now reads the victim org's private repository inventory and can drive posture scans/repo reads via their own workspace. Impact: cross-tenant disclosure of another customer organization's private GitHub repositories and metadata. ## Remediation Bind `installation_id` with the same rigor as `state`: capture it from GitHub's signed post-install redirect (`setup_url`/`callback_url`) and pin it to the pending state at start time, and/or after minting verify the installation's `account` matches the org the initiating workspace is authorized for.

Ecosystems
go
Weakness
CWE-639, CWE-862
Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N
GHSA
GHSA-cp3j-m783-3ph5 (high)

In the news

No ingested article mentions this CVE yet.