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
- Remove the userinfo from the URL and send credentials via headers (e.g. Authorization: Bearer) or the request body instead
- If credentials come from config, move them to a dedicated env var / secret field and build the request in code
- 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
- Never paste curl -u examples into base-URL config
- Store API keys in env vars/secrets, never in URLs
- Lint config files for '@' inside URL authority sections
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- githubapp: request path escaped configured host
- provider upstream URL must not include userinfo
- ssrf: host is blocked (loopback)
- ssrf: scheme not permitted (managed mode requires https)
- awscreds: AWS_CONTAINER_CREDENTIALS_FULL_URI is not a valid…
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)