Skip to content

fix: SSO hardening — OIDC takeover guard, GetSSOLoginURL throttle+GC, SSRF denylist (spec 29 S2/S3/S4)#534

Merged
PaulDotterer merged 2 commits into
mainfrom
fix/sso-hardening-s2-s3-s4
Jul 12, 2026
Merged

fix: SSO hardening — OIDC takeover guard, GetSSOLoginURL throttle+GC, SSRF denylist (spec 29 S2/S3/S4)#534
PaulDotterer merged 2 commits into
mainfrom
fix/sso-hardening-s2-s3-s4

Conversation

@PaulDotterer

Copy link
Copy Markdown
Contributor

Closes #533. Second-pass audit findings S2/S3/S4 (spec 29).

  • S2 — OIDC LinkOrCreate Step 2 refuses to auto-link an IdP-asserted email to a local password account unless TrustEmailAssertions is set (mirrors SCIM createUser WS5 Release 2026.03 #2). A compromised/self-service IdP can no longer seize a pre-existing credential account. Red-checked.
  • S3GetSSOLoginURL (public) added to the per-IP rate-limit ladder; CleanupExpiredAuthStates scheduled hourly. Bounds the storage + outbound-OIDC-discovery DoS.
  • S4 — OIDC HTTP client gets an SSRF dial-control denylist (loopback/RFC1918/ULA/link-local incl. metadata/unspecified), enforced on the resolved dial IP. Guard unit-tested; a package-var seam disables it for the loopback OIDC integration tests.

Tests: S2 linker guard (+ positive-when-trusted), S3 throttle (mirrors ListAuthMethods pattern), S4 dial-control unit test (blocked/allowed/malformed). Full idp+auth suites green.

… SSRF denylist (spec 29 S2/S3/S4)

S2: OIDC auto-link-by-email now refuses to bind an IdP-asserted email to a local
PASSWORD account unless the operator delegated identity via TrustEmailAssertions
— mirrors the SCIM createUser guard, so a compromised/self-service IdP can't
seize a pre-existing credential account. Red-checked.

S3: GetSSOLoginURL (public, the most expensive unauth endpoint) joins the
rate-limit ladder (per-IP), and CleanupExpiredAuthStates is scheduled hourly so
an unauthenticated flood can't grow auth_states unboundedly or amplify outbound
OIDC discovery.

S4: the OIDC HTTP client gets an SSRF dial-control denylist rejecting loopback,
RFC1918/ULA, link-local (incl. 169.254.169.254 metadata), and unspecified
addresses — enforced on the resolved dial IP, so a misconfigured/attacker-set
issuer/token URL can't probe the internal network. Guard logic unit-tested; a
package-var seam disables it for the loopback-backed OIDC integration tests.

#533
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@PaulDotterer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0163d736-90b1-43f9-b08f-b4cb62f13b08

📥 Commits

Reviewing files that changed from the base of the PR and between f6240d4 and 1af6b01.

📒 Files selected for processing (8)
  • cmd/control/main.go
  • internal/auth/interceptor.go
  • internal/auth/interceptor_test.go
  • internal/idp/linker.go
  • internal/idp/linker_test.go
  • internal/idp/main_test.go
  • internal/idp/oidc.go
  • internal/idp/oidc_ssrf_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sso-hardening-s2-s3-s4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements three SSO security hardening findings from spec 29 (S2/S3/S4): an account-takeover guard in the OIDC email auto-linker, a rate limiter and GC sweep for GetSSOLoginURL, and an SSRF denylist on the OIDC HTTP client's dial path.

  • S2 (linker.go): Auto-link-by-email now refuses to bind an IdP-asserted email to a pre-existing local password account unless TrustEmailAssertions is set, mirroring the same guard already in the SCIM createUser path. Returning ErrNoMatchingAccount early also correctly prevents a duplicate-user attempt when AutoCreateUsers is also enabled.
  • S3 (interceptor.go, main.go): GetSSOLoginURL is added to the per-IP rate-limit ladder (10 req/min) with the check placed before the public-procedures bypass, and a new hourly CleanupExpiredAuthStates sweep bounds unbounded auth-state table growth from unauthenticated floods.
  • S4 (oidc.go): ssrfSafeDialControl is wired as a net.Dialer.Control hook that runs after DNS resolution, blocking loopback, RFC 1918, ULA, link-local, unspecified, and RFC 6598 CGNAT addresses. A package-var seam disables the guard in the test binary (consistent with the existing oidcHTTPTimeout pattern) while direct unit tests cover the guard's logic independently.

Confidence Score: 5/5

All three changes are narrowly scoped security fixes with no behaviour changes on the happy path; each has targeted tests covering the guarded branches.

The S2 guard only fires on the new HasPassword && !TrustEmailAssertions branch — existing auto-links (Step 1) and non-password accounts are unaffected. The S3 rate limiter is additive and optional (nil-safe). The S4 SSRF guard is disabled for the test binary via a well-understood package-var seam, consistent with oidcHTTPTimeout. No regressions are apparent in any of the three paths.

No files require special attention — all changes are self-contained and well-tested.

Important Files Changed

Filename Overview
internal/idp/linker.go Adds S2 account-takeover guard: refuses auto-link by email to a local password account unless TrustEmailAssertions is set. Guard correctly short-circuits before Step 3, preventing duplicate-user attempts too.
internal/idp/linker_test.go Adds two new tests pinning the S2 guard: refused auto-link for password accounts without trust, and positive pass-through when TrustEmailAssertions is set. Both use existing mock structure correctly.
internal/idp/oidc.go Adds ssrfSafeDialControl as a net.Dialer.Control hook blocking loopback/RFC1918/ULA/link-local/unspecified/CGNAT addresses after DNS resolution. Package var seam allows test binary to disable the guard for loopback httptest servers.
internal/idp/oidc_ssrf_test.go Unit tests for the SSRF dial-control covering blocked addresses (loopback, RFC1918, link-local, CGNAT, unspecified) and allowed public addresses. Malformed inputs correctly fail closed.
internal/idp/main_test.go TestMain disables the SSRF dial-control for the whole idp test binary so loopback httptest servers remain reachable. Consistent with the existing oidcHTTPTimeout seam pattern.
internal/auth/interceptor.go Adds SSO rate limiter field to RateLimiters and wires the GetSSOLoginURL check before the public-procedures bypass. Ordering is correct: rate check fires first, then the existing public-path handler runs on pass.
internal/auth/interceptor_test.go New test mirrors the ListAuthMethods throttle pattern: 3 requests succeed, the 4th trips ResourceExhausted. Structure is consistent with the existing suite.
cmd/control/main.go Adds CleanupExpiredAuthStates hourly sweep alongside the existing revocation sweep, and registers the SSO rate limiter (10 req/min per IP). Both match existing patterns for periodic tasks and rate limiter wiring.

Reviews (2): Last reviewed commit: "fix(idp): block RFC 6598 CGNAT (100.64.0..." | Re-trigger Greptile

Comment thread internal/idp/oidc.go Outdated
Comment thread internal/idp/oidc.go
…Greptile)

net.IP.IsPrivate omits shared address space, which is reachable as internal
services behind CGNAT / in some cloud VPCs. Added an explicit 100.64.0.0/10
check + test cases.

#533
@PaulDotterer PaulDotterer merged commit acf7b8f into main Jul 12, 2026
11 checks passed
@PaulDotterer PaulDotterer deleted the fix/sso-hardening-s2-s3-s4 branch July 12, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spec 29 S2/S3/S4: SSO hardening — OIDC takeover guard, GetSSOLoginURL throttle+GC, SSRF denylist

1 participant