mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-13 08:58:55 +00:00
fix(auth): SEC-001 — gate OIDC discovery through SafeHTTPDialContext + ValidateSafeURL
Sprint 1 unified-master-audit closure. Two OIDC discovery call sites
passed the bare request context to gooidc.NewProvider:
- internal/auth/oidc/test_discovery.go:65 (dry-run validator)
- internal/auth/oidc/service.go:1066 (runtime cache load)
gooidc.NewProvider derives its HTTP client from the context via
oidc.ClientContext; with no override it falls through to
http.DefaultClient — no SSRF guard. An admin with auth.oidc.create
could induce server-side HTTPS egress to loopback (127.0.0.1, ::1),
RFC 1918, link-local (169.254.169.254 — cloud-instance metadata),
and IPv6 link-local (fe80::/10). The companion JWKS reachability
probe was already routed through SafeHTTPDialContext via the
Bundle 5 R6 closure; the discovery + claims path bypassed that.
Fix:
- New internal/auth/oidc/safehttp.go: oidcDiscoveryClient (Transport
DialContext = validation.SafeHTTPDialContext) + SafeOIDCContext
helper. Both call sites now wrap ctx through SafeOIDCContext
before NewProvider runs.
- Defense-in-depth: OIDCProvider.Validate calls
validation.ValidateSafeURL on the IssuerURL after the existing
https/parse checks, refusing reserved-address issuers at
provider-creation time.
- TestDiscovery surfaces the SSRF policy error via the result's
Errors slice up-front (early-fail UX rail) before invoking
NewProvider.
Test seams:
- setup_test.go swaps oidcDiscoveryClient + validateIssuerSSRF
for httptest loopback compatibility, mirroring the existing
jwksProbeClient pattern.
Regression coverage:
- internal/auth/oidc/domain/types_test.go: 5-case table pinning
loopback v4/v6, cloud metadata, link-local v4/v6 rejection.
- internal/auth/oidc/coverage_fill_test.go: same 5 cases against
Service.TestDiscovery via temporarily restoring the production
gate.
Closes SEC-001.
This commit is contained in:
@@ -58,11 +58,31 @@ type TestDiscoveryResult struct {
|
||||
func (s *Service) TestDiscovery(ctx context.Context, issuerURL string) (*TestDiscoveryResult, error) {
|
||||
res := &TestDiscoveryResult{}
|
||||
|
||||
// SEC-001 closure (Sprint 1, 2026-05-16): refuse reserved-address
|
||||
// issuers up-front so operators see a clear policy error instead
|
||||
// of the lower-level dial-rejection wrap from SafeHTTPDialContext.
|
||||
// The dial-time guard remains the authoritative DNS-rebinding-safe
|
||||
// defense; this is the early-fail UX rail. Routed through the
|
||||
// validateIssuerSSRF package-level seam so tests using
|
||||
// httptest.NewServer can swap it for a no-op (see setup_test.go).
|
||||
if vErr := validateIssuerSSRF(issuerURL); vErr != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("issuer_url failed SSRF policy: %v", vErr))
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Step 1 — discovery. gooidc.NewProvider fetches
|
||||
// `<issuer>/.well-known/openid-configuration` and runs the iss
|
||||
// match check internally; on failure it returns a fmt-style
|
||||
// wrapped error.
|
||||
provider, err := gooidc.NewProvider(ctx, issuerURL)
|
||||
//
|
||||
// SEC-001 closure (Sprint 1, 2026-05-16): the bare `ctx` is wrapped
|
||||
// in SafeOIDCContext so the discovery fetch + the resulting
|
||||
// Verifier's internal JWKS fetch both run through a transport
|
||||
// whose DialContext is validation.SafeHTTPDialContext. Pre-fix the
|
||||
// default HTTP client could be aimed at loopback / RFC 1918 /
|
||||
// link-local / cloud-metadata addresses via the admin-supplied
|
||||
// issuer URL. See safehttp.go for the full closure note.
|
||||
provider, err := gooidc.NewProvider(SafeOIDCContext(ctx), issuerURL)
|
||||
if err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("discovery fetch failed: %v", err))
|
||||
return res, nil // Non-fatal at this layer; the response carries the per-leg failure.
|
||||
|
||||
Reference in New Issue
Block a user