JuliusBrussee/caveman · critical

githubapp: request path escaped configured host

Error message

githubapp: request path escaped configured host

What it means

This is the final SSRF guard: do() joins baseURL with the path, re-parses the combined target URL, and verifies the scheme matches the configured scheme and the host matches case-insensitively with no userinfo. This error means the path, when appended, redirected the request to a different host or scheme than the configured base — a classic open-redirect/SSRF vector.

Solutions

  1. Escape every dynamic segment with url.PathEscape before interpolating into the path.
  2. Remove or escape '@', '\\', and '/' characters originating from user input in path segments.
  3. Audit the code that builds the path so no concatenated fragment can introduce an authority component.
  4. Log the offending path when this fires — it usually indicates malicious or malformed input worth rejecting upstream.

Example fix

// before
path := "/repos/" + userInput + "/contents"
// after
path := "/repos/" + url.PathEscape(userInput) + "/contents"
Defensive patterns

Strategy: validation

Validate before calling

func safeSegment(s string) (string, error) {
    esc := url.PathEscape(s)
    if strings.ContainsAny(esc, "@\\") || strings.Contains(esc, "//") {
        return "", fmt.Errorf("unsafe path segment %q", s)
    }
    return esc, nil
}

Try / catch

if _, _, err := app.DoToken(ctx, tok, m, path, nil); err != nil {
    if strings.Contains(err.Error(), "escaped configured host") {
        // treat as malicious/malformed input: reject and alert, never retry
    }
}

Prevention

When it happens

Trigger: A path containing encoded trickery that changes the effective host after concatenation, e.g. baseURL "https://api.github.com" with path "/@evil.com/" or "/..\\@evil.com/" or "///evil.com/x" variants that slip past earlier checks; also a baseURL with a trailing component that the path hijacks.

Common situations: Interpolating untrusted user input (repo names, branch refs) into the path without escaping, letting '@' or '.' sequences form a new authority; using a base URL whose trailing slash interacts badly with paths built with unescaped segments.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


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

Appendix: source

Thrown at shared/platform/githubapp/githubapp.go:362

func (a *App) DoToken(ctx context.Context, token, method, path string, body any) (int, []byte, error) {
	return a.do(ctx, "Bearer "+token, method, path, body)
}

func (a *App) do(ctx context.Context, authorization, method, path string, body any) (int, []byte, error) {
	if !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") || strings.Contains(path, "\\") {
		return 0, nil, fmt.Errorf("githubapp: request path must be a single-host absolute path")
	}
	base, err := url.Parse(a.baseURL)
	if err != nil || base.Scheme == "" || base.Host == "" || base.User != nil {
		return 0, nil, fmt.Errorf("githubapp: invalid base URL")
	}
	relative, err := url.ParseRequestURI(path)
	if err != nil || relative.IsAbs() || relative.Host != "" || relative.User != nil {
		return 0, nil, fmt.Errorf("githubapp: invalid request path")
	}
	target, err := url.Parse(a.baseURL + path)
	if err != nil || target.Scheme != base.Scheme || !strings.EqualFold(target.Host, base.Host) || target.User != nil {
		return 0, nil, fmt.Errorf("githubapp: request path escaped configured host")
	}
	var reader io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			return 0, nil, fmt.Errorf("githubapp: marshal request: %w", err)
		}
		reader = bytes.NewReader(b)
	}
	req, err := http.NewRequestWithContext(ctx, method, target.String(), reader)
	if err != nil {
		return 0, nil, fmt.Errorf("githubapp: build request: %w", err)
	}
	req.Header.Set("Authorization", authorization)
	req.Header.Set("Accept", "application/vnd.github+json")
	req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
	if body != nil {
		req.Header.Set("Content-Type", "application/json")

View on GitHub (pinned to 3ee70a1026)