{"record":{"id":"71101d88666da324","repo":"JuliusBrussee/caveman","slug":"githubapp-request-path-must-be-a-single-host-abso","errorCode":null,"errorMessage":"githubapp: request path must be a single-host absolute path","messagePattern":"githubapp: request path must be a single-host absolute path","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/githubapp/githubapp.go","lineNumber":350,"sourceCode":"\t\treturn nil, fmt.Errorf(\"githubapp: decode repository proof content: %w\", err)\n\t}\n\tif len(decoded) > 64<<10 || int64(len(decoded)) != payload.Size {\n\t\treturn nil, fmt.Errorf(\"githubapp: repository proof size mismatch\")\n\t}\n\treturn decoded, nil\n}\n\n// DoToken issues an authenticated GitHub REST call with an installation token and\n// returns the status + raw body for the caller to parse. It is the reusable\n// primitive the worker's PR opener builds the Git Data API flow on, so every\n// GitHub egress goes through the one SSRF-guarded client + fixed base host.\nfunc (a *App) DoToken(ctx context.Context, token, method, path string, body any) (int, []byte, error) {\n\treturn a.do(ctx, \"Bearer \"+token, method, path, body)\n}\n\nfunc (a *App) do(ctx context.Context, authorization, method, path string, body any) (int, []byte, error) {\n\tif !strings.HasPrefix(path, \"/\") || strings.HasPrefix(path, \"//\") || strings.Contains(path, \"\\\\\") {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: request path must be a single-host absolute path\")\n\t}\n\tbase, err := url.Parse(a.baseURL)\n\tif err != nil || base.Scheme == \"\" || base.Host == \"\" || base.User != nil {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: invalid base URL\")\n\t}\n\trelative, err := url.ParseRequestURI(path)\n\tif err != nil || relative.IsAbs() || relative.Host != \"\" || relative.User != nil {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: invalid request path\")\n\t}\n\ttarget, err := url.Parse(a.baseURL + path)\n\tif err != nil || target.Scheme != base.Scheme || !strings.EqualFold(target.Host, base.Host) || target.User != nil {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: request path escaped configured host\")\n\t}\n\tvar reader io.Reader\n\tif body != nil {\n\t\tb, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn 0, nil, fmt.Errorf(\"githubapp: marshal request: %w\", err)","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/shared/platform/githubapp/githubapp.go#L332-L368","documentation":"SSRF guard in do(): the request path must be a single-host absolute path — it must start with exactly one '/', contain no backslashes, and not start with '//'. This blocks protocol-relative URLs, UNC-style escapes, and other path shapes that could redirect the request off the configured GitHub host.","triggerScenarios":"Thrown at shared/platform/githubapp/githubapp.go:350 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Pass plain absolute API paths like /repos/owner/name; never full URLs or paths beginning with '//'","Strip backslashes and normalize the path before calling the API layer"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}