JuliusBrussee/caveman · error

ssrf: scheme not permitted (managed mode requires https)

Error message

ssrf: scheme %q not permitted (managed mode requires https)

What it means

This error comes from the SSRF guard's URL-shape policy in parseValidatedURL. Only https is accepted, except that plain http is tolerated when the caller built the Config with ManagedMode=false (self-hosted). The library enforces TLS-only egress in managed (multi-tenant prod) mode so requests cannot be downgraded or sent in cleartext.

Solutions

  1. Change the URL to use https (and port 443 in managed mode)
  2. If this is a self-hosted deployment that legitimately needs http, construct the Config with ssrf.SelfHostedConfig(...) so ManagedMode is false
  3. If the scheme is user-controlled, reject non-https input before handing it to the library
  4. Check the CAVE_ENV / managed-mode wiring: if you did not intend managed mode, do not use ssrf.ManagedConfig()

Example fix

// before
cfg := ssrf.ManagedConfig()
err := ssrf.ValidateURL(ctx, "http://api.example.com/v1", cfg)

// after
cfg := ssrf.ManagedConfig()
err := ssrf.ValidateURL(ctx, "https://api.example.com/v1", cfg)
Defensive patterns

Strategy: validation

Validate before calling

u, err := url.Parse(raw)
if err != nil || (u.Scheme != "https" && !(u.Scheme == "http" && !managed)) {
    return fmt.Errorf("endpoint must use https in managed mode: got %q scheme", schemeOf(raw))
}

Type guard

func isHTTPSEndpoint(raw string) bool {
    u, err := url.Parse(raw)
    return err == nil && u.Scheme == "https"
}

Try / catch

if err := ssrf.ValidateURL(ctx, endpoint, cfg); err != nil {
    var schemeErr bool
    if strings.Contains(err.Error(), "not permitted") && strings.Contains(err.Error(), "scheme") {
        schemeErr = true // fail fast with config guidance
    }
    return fmt.Errorf("endpoint config invalid (schemeErr=%v): %w", schemeErr, err)
}

Prevention

When it happens

Trigger: Calling ssrf.ValidateURL or ssrf.ValidateURLNoResolve with a URL whose scheme is not https or http — e.g. file://, ftp://, gopher:// — or with an http:// URL while Config.ManagedMode is true.

Common situations: A provider base URL configured as http://localhost:8080 for local development is promoted to a managed-mode deployment; a user-supplied callback/webhook URL using file:// or another exotic scheme; an environment variable defaulting to http:// instead of https://.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/cc09d98e2a9d5174. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/ssrf/ssrf.go:216

		return checkAddr(addr, host, port, cfg)
	}
	if strings.EqualFold(host, "localhost") && !(!cfg.ManagedMode && isInAllowList(host, port, cfg.AllowList)) {
		return fmt.Errorf("ssrf: host %q is blocked (loopback)", host)
	}
	return nil
}

// parseValidatedURL applies the URL-shape policy — scheme, embedded
// credentials, managed-mode port — and returns the host/port to check.
func parseValidatedURL(raw string, cfg Config) (host, port string, err error) {
	u, parseErr := url.Parse(raw)
	if parseErr != nil {
		// net/url.Error includes the raw URL (and may therefore include
		// credentials or query secrets). Keep this error field-only and stable.
		return "", "", errors.New("ssrf: invalid URL")
	}
	if u.Scheme != "https" && !(u.Scheme == "http" && !cfg.ManagedMode) {
		return "", "", fmt.Errorf("ssrf: scheme %q not permitted (managed mode requires https)", u.Scheme)
	}
	if u.User != nil {
		return "", "", fmt.Errorf("ssrf: credentials embedded in URL are forbidden")
	}
	host = u.Hostname()
	if host == "" {
		return "", "", fmt.Errorf("ssrf: URL must contain a host")
	}
	port = u.Port()
	if cfg.ManagedMode && port != "" && port != "443" {
		return "", "", errors.New("ssrf: managed mode requires port 443")
	}
	if port == "" {
		if u.Scheme == "https" {
			port = "443"
		} else {
			port = "80"
		}

View on GitHub (pinned to 3ee70a1026)