{"record":{"id":"cb276272fe327f79","repo":"JuliusBrussee/caveman","slug":"ssrf-destination-s-for-host-q-is-a-private-ad-cb2762","errorCode":null,"errorMessage":"ssrf: destination %s (for host %q) is a private address; add %s to the SSRF allowlist (CAVE_SSRF_ALLOWLIST) to permit it","messagePattern":"ssrf: destination (.+?) \\(for host %q\\) is a private address; add (.+?) to the SSRF allowlist \\(CAVE_SSRF_ALLOWLIST\\) to permit it","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/ssrf/ssrf.go","lineNumber":316,"sourceCode":"\t}\n\n\tfor _, p := range blockedPrefixes {\n\t\tif p.Contains(addr) {\n\t\t\t// These ranges (link-local/metadata, ULA outside the narrow local-TUN\n\t\t\t// exception, multicast, unspecified, documentation) are absolutely\n\t\t\t// blocked — no allowlist escape in any mode.\n\t\t\treturn fmt.Errorf(\"ssrf: destination %s (for host %q) is in blocked range %s\", addr, host, p)\n\t\t}\n\t}\n\n\tif inRFC1918(addr) {\n\t\tif cfg.ManagedMode {\n\t\t\treturn fmt.Errorf(\"ssrf: destination %s (for host %q) is a private address blocked in managed mode\", addr, host)\n\t\t}\n\t\t// In self-hosted mode, RFC1918 is blocked unless the original hostname\n\t\t// OR the resolved IP literal appears in the allowlist.\n\t\tif !isInAllowList(host, port, cfg.AllowList) && !isInAllowList(addr.String(), port, cfg.AllowList) {\n\t\t\treturn fmt.Errorf(\"ssrf: destination %s (for host %q) is a private address; add %s to the SSRF allowlist (CAVE_SSRF_ALLOWLIST) to permit it\", addr, host, allowListSuggestion(addr, port))\n\t\t}\n\t}\n\treturn nil\n}\n\n// allowListSuggestion renders the allowlist entry to advise for a blocked\n// destination. It must be an entry isInAllowList would actually accept at BOTH\n// stages: ValidateHost pre-flights with an empty port, so JoinHostPort would\n// emit a trailing-colon token like \"127.0.0.1:\" that matches only the\n// port-less stage — an operator following that advice literally relaxes the\n// pre-flight guard, is blocked again at dial time by a second message naming a\n// different token, and leaves a stale weakening entry behind. The bare address\n// form is the one entry that matches every stage.\nfunc allowListSuggestion(addr netip.Addr, port string) string {\n\tif port == \"\" {\n\t\treturn addr.String()\n\t}\n\treturn net.JoinHostPort(addr.String(), port)","sourceCodeStart":298,"sourceCodeEnd":334,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/shared/platform/ssrf/ssrf.go#L298-L334","documentation":"checkAddr blocked an RFC1918 private destination in self-hosted mode because no allowlist entry matches. The message names CAVE_SSRF_ALLOWLIST so operators with legitimate internal endpoints can opt in explicitly.","triggerScenarios":"Thrown at shared/platform/ssrf/ssrf.go:316 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Add the original hostname or its IP to CAVE_SSRF_ALLOWLIST if the internal endpoint is intentional","Otherwise point the request at the public address of the service","Verify the allowlist entry format matches host and port"],"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-14T00:17:10.932Z"}