Skip to content

fix(auth): rate-limit the GitHub token-retrieval endpoint by session, not IP - #6135

Merged
JSONbored merged 1 commit into
mainfrom
security/auth-token-review-6117
Jul 15, 2026
Merged

fix(auth): rate-limit the GitHub token-retrieval endpoint by session, not IP#6135
JSONbored merged 1 commit into
mainfrom
security/auth-token-review-6117

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Security review pass over #6114/#6115/#6116's persisted-GitHub-token storage, retrieval, and revocation surface — the final issue in the "Unified AMS/ORB Auth" milestone.

Review findings (pass/fail per the issue's checklist)

Item Result
Encryption at rest is correct, keys managed like other secrets PassstoreSessionGitHubToken/getDecryptedSessionGitHubTokenBundle reuse encryptSecret/decryptSecret (AES-256-GCM, PBKDF2-derived key, random per-record IV+salt), the same primitive and key material as repositoryAiKeys/repositoryLinearKeys.
No path ever includes the raw token (logs, audit events, telemetry, Sentry) PassstoreSessionGitHubToken's one console.warn logs sessionId/message only; the route's recordRouteProductUsage call carries no token field; packages/loopover-miner/lib/github-token-resolution.js never logs the resolved token. Sentry's existing beforeSend scrubber (src/selfhost/sentry.ts) independently catches GitHub token value patterns (gh[opsru]_...) as defense in depth. An existing test asserts the raw token never appears in the auth.session_created audit event.
Logout/revocation genuinely deletes the stored token, verified by test PassrevokeAuthSession unconditionally calls deleteSessionGitHubToken; an existing test checks both the decrypt-returns-null path and a raw row-count query.
Retrieval endpoint unreachable by non-session identities (re-verified against static mcp/api) Pass — an existing test explicitly authenticates as the static api/mcp shared-secret identities and confirms both get 403.
Refresh tokens get the same at-rest protection as access tokens Pass — refresh tokens go through the identical encryptSecret call as access tokens, not a weaker scheme.
Rate-limit/abuse posture on the retrieval endpoint Fail — fixed in this PR. See below.

The fix

isPreAuthRateLimitPath in src/auth/rate-limit.ts classified every /v1/auth/* path as pre-authentication, keying its rate limit by client IP. That's correct for the OAuth start/callback/device-poll flows it sits alongside, but /v1/auth/github/token always requires (and validates) a real session bearer token to do anything useful. IP-keying meant:

  • A caller holding a stolen session token could exceed the strict 10/min cap by rotating source IPs.
  • Unrelated sessions behind a shared IP (office NAT, CI infra) would throttle each other.

Excluded /v1/auth/github/token from the pre-auth classification so it falls through to token-based keying when a valid bearer is present, falling back to IP-keying only when no valid bearer is supplied — matching every other authenticated route.

Also confirmed the only production caller of the decrypted-token repository functions is this single route (via getLiveSessionGitHubToken) — no other route or admin surface reaches the decrypted token.

Out of scope

The identical IP-vs-session gap exists on the pre-existing (pre-milestone, #556) /v1/auth/extension/session endpoint. That predates this milestone and is tracked as a separate follow-up rather than bundled here.

Closes #6117

Test plan

  • New test in test/unit/auth.test.ts: same session token from two IPs shares one rate-limit bucket; a different session's token from the same IP gets an independent bucket; no/invalid bearer still falls back to IP-keying
  • routeClassForPath("/v1/auth/github/token") explicitly asserted as "strict"
  • Full local npm run test:ci gate green (unsharded coverage, no threshold failures)
  • npm run typecheck clean

… not IP

Closes #6117

Security review pass over #6114/#6115/#6116's persisted-GitHub-token
storage, retrieval, and revocation surface.

Findings:
- Encryption at rest: correct. storeSessionGitHubToken/
  getDecryptedSessionGitHubTokenBundle reuse encryptSecret/decryptSecret
  (AES-256-GCM, PBKDF2-derived key, random per-record IV+salt) -- the same
  primitive and key material as repositoryAiKeys/repositoryLinearKeys.
  Refresh tokens go through the identical encryptSecret call as access
  tokens, not a weaker scheme.
- No raw-token leakage: verified across storeSessionGitHubToken (its one
  console.warn logs sessionId/message only), the /v1/auth/github/token
  route (recordRouteProductUsage carries no token field), and
  packages/loopover-miner/lib/github-token-resolution.js (no console/log
  call ever touches the resolved token). Sentry's existing beforeSend
  scrubber (src/selfhost/sentry.ts) independently catches GitHub token
  value patterns (gh[opsru]_...) as defense in depth. An existing test
  already asserts the raw token never appears in the auth.session_created
  audit event.
- Logout/revocation: revokeAuthSession unconditionally calls
  deleteSessionGitHubToken; already verified by an existing test that
  checks both the decrypt-returns-null path and a raw row-count query.
- Endpoint access control: session-only, already verified by an existing
  test that explicitly authenticates as the static "api"/"mcp" shared-secret
  identities and confirms both are rejected with 403.
- Rate-limit/abuse posture: REAL GAP, fixed here. isPreAuthRateLimitPath's
  broad `/v1/auth/` prefix match classified /v1/auth/github/token as
  pre-auth, keying its rate limit by client IP instead of by session --
  unlike the OAuth start/callback/device-poll flows it sits alongside, this
  endpoint always requires a valid session bearer. IP-keying meant a caller
  with a stolen session token could exceed the strict 10/min cap by
  rotating source IPs, and unrelated sessions behind a shared IP (office
  NAT, CI infra) would throttle each other. Excluded this one path from the
  pre-auth classification so it falls through to token-based keying when a
  valid bearer is present (falling back to IP-keying only when no valid
  bearer is supplied, same as every other authenticated route).

Also confirmed the only production caller of the decrypted-token repository
functions is the single /v1/auth/github/token route (via
getLiveSessionGitHubToken) -- no other route or admin surface reaches the
decrypted token.

The identical IP-vs-session rate-limit gap exists on the pre-existing (pre-
milestone) /v1/auth/extension/session endpoint; that is out of this
milestone's scope and tracked separately.
@superagent-security

Copy link
Copy Markdown
Contributor

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

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.33%. Comparing base (e94ba61) to head (4442153).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #6135   +/-   ##
=======================================
  Coverage   95.33%   95.33%           
=======================================
  Files         598      598           
  Lines       47178    47178           
  Branches    15026    15026           
=======================================
  Hits        44975    44975           
  Misses       1477     1477           
  Partials      726      726           
Flag Coverage Δ
shard-1 43.93% <0.00%> (-0.01%) ⬇️
shard-2 36.68% <100.00%> (+0.01%) ⬆️
shard-3 32.11% <100.00%> (-0.05%) ⬇️
shard-4 34.00% <0.00%> (-0.15%) ⬇️
shard-5 31.45% <0.00%> (-0.76%) ⬇️
shard-6 45.23% <100.00%> (+0.33%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/auth/rate-limit.ts 98.88% <100.00%> (ø)

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 15, 2026
@JSONbored JSONbored self-assigned this Jul 15, 2026
@JSONbored
JSONbored merged commit 90da839 into main Jul 15, 2026
17 checks passed
@JSONbored
JSONbored deleted the security/auth-token-review-6117 branch July 15, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security review pass: persisted GitHub token storage, retrieval, and revocation

1 participant