JuliusBrussee/caveman · error
githubapp: base_url rejected by SSRF guard
Error message
githubapp: base_url rejected by SSRF guard: %w
What it means
githubapp.New performs a pre-flight SSRF check (ssrf.ValidateURL) on a custom base URL (GitHub Enterprise) before constructing its HTTP client, and this error wraps the guard's rejection. It protects the production path from pointing the App at internal/private network addresses. Callers that inject their own client (tests) bypass the check because they own host policy.
Solutions
- Use an HTTPS, publicly resolvable URL for the GHE instance (the SSRF guard blocks private/loopback ranges by design)
- For local testing, inject your own *http.Client into Config (the pre-flight is skipped when a client is provided)
- If GHE sits behind a proxy/allowlist, configure the ssrf ManagedConfig allowlist appropriately instead of weakening the guard
- Verify the base URL has no scheme typos (e.g. http instead of https) and no internal-only hostnames
Example fix
// before
base := "http://ghe.internal.corp/api/v3"
app, err := githubapp.New(githubapp.Config{AppID: id, PrivateKeyPEM: pem, BaseURL: base})
// after
base := "https://ghe.mycompany.example/api/v3"
app, err := githubapp.New(githubapp.Config{AppID: id, PrivateKeyPEM: pem, BaseURL: base}) Defensive patterns
Strategy: try-catch
Validate before calling
u, err := url.Parse(base)
if err != nil || u.Scheme != "https" {
return fmt.Errorf("base URL must be https: %q", base)
} Try / catch
app, err := githubapp.New(cfg)
if err != nil && strings.Contains(err.Error(), "SSRF guard") {
// base URL points at a disallowed (private/non-https) host;
// fix the URL or, for tests, inject a custom client
return ErrBaseURLRejected
} Prevention
- Always use public HTTPS URLs for GHE in production
- Use client injection for local/mock testing instead of private base URLs
- Keep the SSRF allowlist updated when adding new GHE endpoints
When it happens
Trigger: Calling New with a non-nil default client and cfg.BaseURL set to something other than the default api.github.com base, where ssrf.ValidateURL rejects the URL — private/loopback/link-local IPs, non-HTTPS schemes, disallowed hosts.
Common situations: Pointing the base URL at a local GHE dev box or mock server via production code path; a config value like http://localhost:8080/api/v3; DNS that resolves a hostname to a private address; typo'd internal URLs.
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
- ssrf: host is blocked (loopback)
- ambiguous escaped path sequence
- awscreds: refusing plaintext container credentials endpoint…
- awscreds: refusing plaintext IMDS endpoint at host
- backslash is not allowed in path
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/10e0589a6dbd693d.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/githubapp/githubapp.go:89
if len(cfg.PrivateKeyPEM) == 0 {
return nil, fmt.Errorf("githubapp: private key PEM is required")
}
key, err := parseRSAPrivateKey(cfg.PrivateKeyPEM)
if err != nil {
return nil, err
}
base := strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
if base == "" {
base = defaultBaseURL
}
client := cfg.HTTPClient
if client == nil {
// Production path: SSRF-guarded client + pre-flight host check on a custom
// (GHE) base. When a caller injects a client (tests), it owns the host policy,
// so we skip the pre-flight — but production never injects one.
if base != defaultBaseURL {
if err := ssrf.ValidateURL(context.Background(), base, ssrf.ManagedConfig()); err != nil {
return nil, fmt.Errorf("githubapp: base_url rejected by SSRF guard: %w", err)
}
}
client = ssrf.NewHTTPClient(ssrf.ManagedConfig())
client.Timeout = 20 * time.Second
}
return &App{
appID: strings.TrimSpace(cfg.AppID),
slug: strings.TrimSpace(cfg.AppSlug),
privateKey: key,
webhookSecret: cfg.WebhookSecret,
baseURL: base,
httpClient: client,
}, nil
}
// Slug returns the App slug used to build the install URL.
func (a *App) Slug() string { return a.slug }
View on GitHub (pinned to 3ee70a1026)