JuliusBrussee/caveman · error

ssrf: credentials embedded in URL are forbidden

Error message

ssrf: credentials embedded in URL are forbidden

What it means

parseValidatedURL rejects any URL that carries a userinfo component (user:pass@host). Credentials in URLs leak into logs, proxies, and error messages, so this library flatly forbids them instead of passing them through.

Solutions

  1. Remove the userinfo from the URL and send credentials via headers (e.g. Authorization: Bearer) or the request body instead
  2. If credentials come from config, move them to a dedicated env var / secret field and build the request in code
  3. Pre-validate URLs and reject any containing '@' before the userinfo part

Example fix

// before
endpoint := "https://user:sk-123@api.example.com"
req, _ := http.NewRequest("POST", endpoint, body)
req.SetBasicAuth("user", "sk-123")

// after
endpoint := "https://api.example.com"
req, _ := http.NewRequest("POST", endpoint, body)
req.SetBasicAuth("user", "sk-123")
Defensive patterns

Strategy: validation

Validate before calling

if u, err := url.Parse(raw); err != nil || u.User != nil {
    return errors.New("URL must not contain embedded credentials; use headers instead")
}

Type guard

func hasURLCredentials(raw string) bool {
    u, err := url.Parse(raw)
    return err == nil && u.User != nil
}

Try / catch

if err := ssrf.ValidateURL(ctx, endpoint, cfg); err != nil {
    if strings.Contains(err.Error(), "credentials embedded in URL") {
        return fmt.Errorf("strip userinfo from endpoint %q and pass it via Authorization header", redactHost(endpoint))
    }
    return err
}

Prevention

When it happens

Trigger: Calling ssrf.ValidateURL / ValidateURLNoResolve with a URL like https://user:token@api.example.com — i.e. url.URL.User is non-nil after parsing.

Common situations: Copying a provider endpoint from docs or a curl example that embeds an API key in the URL; migrating from a service that accepted basic-auth-in-URL; templating a base URL with credentials interpolated.

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/572892170ea8f795. Report an issue: GitHub.

Appendix: source

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

		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"
		}
	}
	return host, port, nil
}

View on GitHub (pinned to 3ee70a1026)