Skip to content

scheduledauth: enforce HTTPS scheme on SCHEDULED_TASK_OIDC_JWKS_URL #66

Description

@cristim

Context

Spun out of the adversarial review of PR LeanerCloud/cloud-commitments-cli#1231 (SEC-04, hardened HTTP client for JWKS warmup and fetch). Filed separately because the in-scope SEC-04 work was the move from providers/azure/internal/httpclient to pkg/httpclient with IMDS-blocking parity; this is additional hardening on top.

Gap

internal/server/scheduledauth.validateAbsoluteURL accepts any scheme with a non-empty host:

func validateAbsoluteURL(raw, label string) error {
	u, err := url.Parse(raw)
	if err != nil {
		return fmt.Errorf("%w: %s must be an absolute URL: %v", ErrConfigInvalid, label, err)
	}
	if u.Scheme == "" || u.Host == "" {
		return fmt.Errorf("%w: %s must be an absolute URL", ErrConfigInvalid, label)
	}
	return nil
}

So SCHEDULED_TASK_OIDC_JWKS_URL=http://www.googleapis.com/... would pass. JWKS responses sign every inbound JWT in oidc mode; cleartext retrieval is a classic MITM vector.

Fix sketch

In validateAbsoluteURL (or a dedicated validator for the JWKS URL specifically), require u.Scheme == "https". Test that:

  • http://... is rejected at startup with a clear error
  • https://... is accepted
  • An empty value still falls back to GoogleJWKSURL (which is already https)

Closing-issue trailer for the fix PR

Closes #<this issue number>

Activity

  1. cristim commented on Jul 28, 2026

    @cristim
    MemberAuthor

    Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before changing code, since a finding may have been fixed or moved.

    The 2026-07-28 full-repo review re-derived this independently and confirmed it is still present. Two additions: an in-tree precedent that settles the "should we require https" question, and the reachability argument, which suggests severity/medium is understated.

    Where

    • internal/server/scheduledauth/validator.go:217 (validateAbsoluteURL), called from internal/server/scheduledauth/validator.go:145
    • Contrast: internal/oidc/issuer_cache.go:38 (oidc.SetIssuerURL)

    What the review adds

    The codebase already enforces the stricter rule one package over. oidc.SetIssuerURL rejects anything that is not an absolute https URL. So CUDly enforces https on its own issuer URL and not on the URL it fetches trusted verification keys from. That asymmetry is the whole finding, and it means the fix is a copy of an existing validator rather than a new policy decision.

    The blast radius is money movement, not just token forgery. /api/scheduled/* is the surface that fires purchase executions. If an attacker substitutes the JWKS, go-oidc verifies attacker-signed RS256 tokens as genuine; issuer, audience and subject are all string claims inside a token the attacker now controls, so every remaining check passes. There is no second gate behind the token.

    Failure scenario

    An operator sets SCHEDULED_TASK_OIDC_JWKS_URL to an http:// endpoint: an internal mirror, a cached copy behind a corporate proxy, or a test double left in place after a staging exercise. An attacker with a network position on that path, or DNS control over the host, serves their own JWKS. Every subsequent /api/scheduled/* call they make is accepted as an authenticated scheduled task, which yields unauthenticated purchase execution.

    The failure is silent in both directions: the operator's config looks correct, and nothing in the logs distinguishes a substituted key set from a legitimate one.

    Fix direction

    As already sketched here: require u.Scheme == "https" in validateAbsoluteURL, with an explicit localhost carve-out if the test suite needs one, matching SetIssuerURL. Given the effort is xs and the outcome is unauthenticated money movement, this looks worth pulling forward from urgency/this-quarter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions