{"id":"CVE-2026-59185","aliases":["GHSA-cp3j-m783-3ph5","GO-2026-6448"],"title":"Identrail Cross-tenant IDOR: Client-supplied GitHub App installation_id is bound to the caller's workspace without ownership verification","summary":"Identrail Cross-tenant IDOR: Client-supplied GitHub App installation_id is bound to the caller's workspace without ownership verification","severity":"high","cvss":8.5,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N","vendor":"identrail","product":"github.com/identrail/identrail","ecosystem":"go","affected":["github.com/identrail/identrail < 1.0.2"],"patched":["github.com/identrail/identrail 1.0.2"],"published":"2026-09-09","updated":"2026-09-17","sourceUpdated":"2026-09-17T17:40:57.250745573Z","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-cp3j-m783-3ph5","references":[{"url":"https://github.com/identrail/identrail/security/advisories/GHSA-cp3j-m783-3ph5"},{"url":"https://github.com/identrail/identrail/commit/835e40517509d6ef5405c27fbf14f579bedff0e7"},{"url":"https://github.com/identrail/identrail"},{"url":"https://github.com/identrail/identrail/releases/tag/v1.0.2"},{"url":"https://github.com/advisories/GHSA-cp3j-m783-3ph5"}],"tags":["osv","go","ghsa"],"cwe":["CWE-639","CWE-862"],"ingestedAt":"2026-09-10T00:26:24.579Z","slug":"CVE-2026-59185","body":"## Overview\n\n## Summary\n\nidentrail'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.\n\n## Affected code (default-on path, no feature flag)\n\n- 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`).\n- 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`).\n- 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.\n\nThe 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.\n\n## Intent proof\n\nThe 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.\n\n## Exploitation\n\n1. Attacker is any authenticated identrail tenant; they call `StartGitHubConnection` for their own workspace and receive a `state`.\n2. 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).\n3. `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.\n4. Attacker now reads the victim org's private repository inventory and can drive posture scans/repo reads via their own workspace.\n\nImpact: cross-tenant disclosure of another customer organization's private GitHub repositories and metadata.\n\n## Remediation\n\nBind `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.\n\n## Affected packages\n\n- `github.com/identrail/identrail < 1.0.2`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `github.com/identrail/identrail 1.0.2`","depth":"twilight","depthScore":47,"depthScoreParts":{"impact":46.8,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}