# Identrail Cross-tenant IDOR: Client-supplied GitHub App installation_id is bound to the caller's workspace without ownership verification

identrail's GitHub App connect-complete endpoint trusted a client-supplied installation_id, letting any tenant mint a token for another org's installation and read its private repos.

- Payout: Research
- Severity: high
- Program: identrail
- Found by: CyberKareem
- Disclosed: 2026-09-09
- CVE: CVE-2026-59185
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-cp3j-m783-3ph5
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-cp3j-m783-3ph5
- Tags: area:web domain:api domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:idor

**Target:** GitHub App connection-completion API endpoint (POST /v1/workspaces/:workspace_id/projects/:project_id/github/connect/complete)
**Root cause:** The completion handler rigorously binds the OAuth `state` to tenant/workspace/project but only validates `installation_id > 0`, persisting the raw client value and minting an installation access token for it without verifying ownership.
**Steps:**
1. Authenticate as any tenant and call StartGitHubConnection for own workspace to obtain a `state`.
2. Call `POST /v1/workspaces/:workspace_id/projects/:project_id/github/connect/complete` with `{state: <own state>, installation_id: V}` (or `X-GitHub-Installation-ID: V`) where V is a victim org's installation id.
3. Handler passes the state scope check at `github_connect.go:818` and persists V as the workspace connection (`:840`).
4. `ListInstallationRepositories(V)` mints a token via `POST /app/installations/{installationId}/access_tokens` signed with the App JWT and lists the victim org's private repos.
**How found:** Code review of the connect-complete flow: noticed `state` was bound to {TenantID, WorkspaceID, ProjectID} and re-verified at completion, while `installation_id` had only a `<= 0` check — the asymmetry between the two client-supplied values revealed the missing ownership check.
**Exploitation:** Attacker needs only a valid tenant account and a victim installation id (non-secret, enumerable integer visible in post-install redirect URLs, webhook payloads, and org GitHub App settings). They complete their own connect flow with the victim's installation_id; identrail mints an installation token for it and binds it to the attacker's workspace, giving persistent read access to the victim org's private repository inventory and metadata via posture scans/repo reads.
**Insight:** When a flow has two client-supplied identifiers (state + resource id), check whether both are bound to the caller's scope; a rigorously validated state often masks an unvalidated resource id that is then used to mint credentials for that resource.

