Skip to content

fix(orb): bind enrollment admin checks to immutable GitHub account ids#1393

Merged
JSONbored merged 5 commits into
mainfrom
codex/fix-oauth-self-enrollment-login-vulnerability
Jun 27, 2026
Merged

fix(orb): bind enrollment admin checks to immutable GitHub account ids#1393
JSONbored merged 5 commits into
mainfrom
codex/fix-oauth-self-enrollment-login-vulnerability

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Motivation

  • Prevent a privilege-escalation where OAuth self-enrollment authorized by a stored mutable account_login could be abused if a GitHub account/org renames or an attacker reclaims the old login.
  • Ensure enrollment authorization is tied to the immutable GitHub account id so a recycled login cannot enroll a victim installation_id.

Description

  • Add a migration migrations/0070_orb_github_installation_account_id.sql to persist account_id in orb_github_installations.
  • Persist account_id from webhook upserts and the App-installation backfill by wiring it through src/orb/installations.ts and src/orb/app-auth.ts.
  • Harden the admin gate in src/orb/oauth.ts by threading the OAuth user.id and stored account_id into verifyInstallationAdmin and requiring id-equality for User installs and matching organization.id for Org installs.
  • Read account_id in the OAuth enrollment flow and pass the authenticated user.id into the authorization check, and add a regression test that verifies stale-login reuse is refused; update related tests to exercise the new branches.

Testing

  • Ran npx vitest run test/integration/orb-oauth.test.ts test/integration/orb-installations.test.ts test/unit/orb-app-auth.test.ts and all specified tests passed.
  • Ran npm run db:migrations:check which passed and npm run typecheck which passed with no type errors.
  • Attempted the full gate via npm run test:ci but the run failed during coverage generation with TypeError: jsTokens is not a function from the coverage tooling (failure appears unrelated to the orb change and blocked full CI completion).
  • npm audit --audit-level=moderate could not complete in this environment (registry audit returned 403).

Codex Task

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 25, 2026
@superagent-security

Copy link
Copy Markdown

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.42%. Comparing base (6ffc2d0) to head (d373376).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1393      +/-   ##
==========================================
- Coverage   95.43%   95.42%   -0.01%     
==========================================
  Files         202      202              
  Lines       21743    21743              
  Branches     7856     7857       +1     
==========================================
- Hits        20750    20749       -1     
  Misses        416      416              
- Partials      577      578       +1     
Files with missing lines Coverage Δ
src/orb/app-auth.ts 100.00% <100.00%> (ø)
src/orb/installations.ts 100.00% <100.00%> (ø)
src/orb/oauth.ts 97.56% <100.00%> (-2.44%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 25, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
gittensory-ui d373376 Commit Preview URL

Branch Preview URL
Jun 27 2026, 01:03 AM

@JSONbored JSONbored self-assigned this Jun 26, 2026
@JSONbored JSONbored added the gittensor:bug Gittensor-scored bug fix - worth 0.5x multiplier. label Jun 26, 2026
@JSONbored JSONbored force-pushed the codex/fix-oauth-self-enrollment-login-vulnerability branch from e557966 to 780127d Compare June 26, 2026 20:59
@gittensory-orb

gittensory-orb Bot commented Jun 27, 2026

Copy link
Copy Markdown

Tip

🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩

✅ Gittensory review — safe to merge

8 files · 1 AI reviewers · no blockers · readiness 48/100 · CI green · unknown

✅ Approved — safe to merge

Review summary
This PR hardens the OAuth self-enrollment admin gate by binding authorization to GitHub's immutable numeric account ids instead of mutable login strings, closing a privilege-escalation path where a recycled or renamed login could authorize enrollment of a victim's installation. The migration, persistence layer, `verifyInstallationAdmin` signature, and all call sites are updated consistently. For User installs both login (case-insensitive) and id must match; for Org installs the membership API's `organization.id` is validated against the stored `account_id`, so a stale `account_login` in the DB cannot be exploited even after a rename. Existing rows with `account_id IS NULL` (pre-migration) safely fail the `accountId === null` guard, defaulting to deny until a webhook re-sync or backfill populates the column.

Nits (5)

  • test/integration/orb-oauth.test.ts: the test description 'a USER-account owner self-enrolls (a login-only identity stores a null github id)' is now stale — the test now seeds `account_id: 7`, stubs `user.id: 7`, and asserts `maintainer_github_id: 7`; rename it to reflect that User installs now require a matching numeric id.
  • src/orb/oauth.ts verifyInstallationAdmin: for User installs the check is `userId === accountId && userLogin… === accountLogin…` — once id-equality is enforced, the login equality clause is redundant (ids are unique to accounts) and may confuse future readers; a short inline comment noting it as defense-in-depth clarifies intent.
  • test/integration/orb-oauth.test.ts 'refuses stale-login reuse' test: it only asserts status 403 and the absence of an enrollment row; adding `expect(await res.text()).toContain('Admin access required')` would pin the specific failure branch and guard against a future wrong-branch 403 masking the same status.
  • src/orb/oauth.ts verifyInstallationAdmin: tighten the null guard to `accountId == null` (double-equals) so it also catches an accidental `undefined` that could slip through a future refactor of the DB query type, or alternatively narrow the parameter type from `number | null` to `number` and push the null-check to the call site in `handleOrbEnrollment` to make the contract explicit.
  • migrations/0075_orb_github_installation_account_id.sql: add a comment noting that operators must run the backfill (`backfillOrbInstallations`) after applying this migration or OAuth self-enrollment will deny all existing installations until their `account_id` is populated via webhook re-sync.
Signal Result Evidence
Code review ✅ No blockers 1 reviewers, synthesized
Linked issue ⚠️ Missing No linked issue or no-issue rationale found.
Related work ⚠️ 3 scoped overlaps Top overlaps are listed below; lower-confidence bulk is hidden.
Review load ❌ 8/20 Readiness component derived from cached public PR metadata and labels; size label size:M.
Validation evidence ❌ 5/25 Cached preflight status is hold.
Open PR queue ❌ 3/10 26 open PR(s), 9 likely reviewable, 17 unlinked.
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 81 PR(s), 297 issue(s).
Gate result ✅ Passing No configured blocker found.
Nits — 2 non-blocking
  • Repository config was not parsed
  • No linked issue detected — If this PR is intended to solve an issue, link it explicitly in the PR body.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: not available
  • Official Gittensor activity: 81 PR(s), 297 issue(s).
  • Related work: Titles/paths share 7 meaningful terms. (PR #1537, PR #1546)
  • Related work: Titles/paths share 6 meaningful terms. (PR #1492, PR #1554)
  • Related work: Titles/paths share 6 meaningful terms. (PR #1537, PR #1545)
  • Additional title-only matches omitted; title-only overlap does not block.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Explain no-issue PR.
  • Review top overlaps.
  • Add scope summary.
  • Fix blocker.
  • Expect slower review.
  • Refresh registry data or choose a registered active repo.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
  • Check active issues and PRs before submitting.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Review load = cached public PR metadata such as size labels, changed paths, and preflight status.
  • Open PR queue = repo-wide review pressure; it is not a PR quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
Review details

Generated from public PR metadata and the diff. Advisory only; deterministic signals remain authoritative.

This PR hardens the OAuth self-enrollment admin gate by binding authorization to GitHub's immutable numeric account ids instead of mutable login strings, closing a privilege-escalation path where a recycled or renamed login could authorize enrollment of a victim's installation. The migration, persistence layer, `verifyInstallationAdmin` signature, and all call sites are updated consistently. For User installs both login (case-insensitive) and id must match; for Org installs the membership API's `organization.id` is validated against the stored `account_id`, so a stale `account_login` in the DB cannot be exploited even after a rename. Existing rows with `account_id IS NULL` (pre-migration) safely fail the `accountId === null` guard, defaulting to deny until a webhook re-sync or backfill populates the column.

Nits (5)

  • test/integration/orb-oauth.test.ts: the test description 'a USER-account owner self-enrolls (a login-only identity stores a null github id)' is now stale — the test now seeds `account_id: 7`, stubs `user.id: 7`, and asserts `maintainer_github_id: 7`; rename it to reflect that User installs now require a matching numeric id.
  • src/orb/oauth.ts verifyInstallationAdmin: for User installs the check is `userId === accountId && userLogin… === accountLogin…` — once id-equality is enforced, the login equality clause is redundant (ids are unique to accounts) and may confuse future readers; a short inline comment noting it as defense-in-depth clarifies intent.
  • test/integration/orb-oauth.test.ts 'refuses stale-login reuse' test: it only asserts status 403 and the absence of an enrollment row; adding `expect(await res.text()).toContain('Admin access required')` would pin the specific failure branch and guard against a future wrong-branch 403 masking the same status.
  • src/orb/oauth.ts verifyInstallationAdmin: tighten the null guard to `accountId == null` (double-equals) so it also catches an accidental `undefined` that could slip through a future refactor of the DB query type, or alternatively narrow the parameter type from `number | null` to `number` and push the null-check to the call site in `handleOrbEnrollment` to make the contract explicit.
  • migrations/0075_orb_github_installation_account_id.sql: add a comment noting that operators must run the backfill (`backfillOrbInstallations`) after applying this migration or OAuth self-enrollment will deny all existing installations until their `account_id` is populated via webhook re-sync.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@gittensory-orb gittensory-orb Bot added the gittensor Gittensor contributor context label Jun 27, 2026
@JSONbored JSONbored merged commit f2cfb1e into main Jun 27, 2026
20 checks passed
@JSONbored JSONbored deleted the codex/fix-oauth-self-enrollment-login-vulnerability branch June 27, 2026 01:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aardvark codex gittensor:bug Gittensor-scored bug fix - worth 0.5x multiplier. gittensor Gittensor contributor context size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant