{"record":{"id":"a7e134e040ff94bd","repo":"googleapis/mcp-toolbox","slug":"url-host-must-be-an-allowed-fhir-host-got-q","errorCode":null,"errorMessage":"URL host must be an allowed FHIR host, got %q","messagePattern":"URL host must be an allowed FHIR host, got %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sources/cloudhealthcare/cloud_healthcare.go","lineNumber":340,"sourceCode":"}\n\nfunc (s *Source) validateFHIRPageURL(pageURL string) (string, error) {\n\tparsed, err := url.Parse(pageURL)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"invalid page URL: %w\", err)\n\t}\n\n\tif parsed.Scheme != \"https\" {\n\t\treturn \"\", fmt.Errorf(\"URL scheme must be https, got %q\", parsed.Scheme)\n\t}\n\n\tparsed.Host = strings.ToLower(parsed.Host)\n\thost := parsed.Host\n\tif h, _, err := net.SplitHostPort(host); err == nil {\n\t\thost = h\n\t}\n\tif _, ok := allowedFHIRHosts[host]; !ok {\n\t\treturn \"\", fmt.Errorf(\"URL host must be an allowed FHIR host, got %q\", host)\n\t}\n\n\t// Clean and split path\n\tcleanPath := path.Clean(parsed.Path)\n\t// Truncate leading and trailing slashes for easier splitting\n\ttrimmed := strings.Trim(cleanPath, \"/\")\n\tparts := strings.Split(trimmed, \"/\")\n\n\t// Page URL format Reference: https://docs.cloud.google.com/healthcare-api/docs/how-tos/fhir-search#using_the_search_method_with_get\n\tif len(parts) < 10 {\n\t\treturn \"\", fmt.Errorf(\"invalid FHIR URL path structure: path too short\")\n\t}\n\n\tif !isValidAPIVersion(parts[0]) {\n\t\treturn \"\", fmt.Errorf(\"invalid API version prefix: %q\", parts[0])\n\t}\n\n\tif parts[1] != \"projects\" {","sourceCodeStart":322,"sourceCodeEnd":358,"githubUrl":"https://github.com/googleapis/mcp-toolbox/blob/8cc6e09de2ad7b8bffc77751799585a1401a48eb/internal/sources/cloudhealthcare/cloud_healthcare.go#L322-L358","documentation":"validateFHIRPageURL lowercases the host (stripping any port) and checks it against the allowedFHIRHosts allowlist (healthcare.googleapis.com and healthcare.mtls.googleapis.com). This error means the pagination URL points at a host that is not a genuine Google Healthcare API endpoint — a defense against SSRF where a malicious or buggy page link could make the server fetch arbitrary internal URLs.","triggerScenarios":"A page URL with a substituted host (e.g. evil.example.com, an internal 10.x hostname, or metadata.google.internal) is passed to FHIRFetchPage; regional endpoints or a custom service endpoint host were pasted into the page URL.","commonSituations":"Prompt-injection or poisoned API responses tricking an agent into fetching an attacker URL; developers pointing page URLs at a self-hosted FHIR proxy; mixing up the FHIR store's selfLink host with the API host.","solutions":["Only pass page URLs verbatim from previous Cloud Healthcare API responses (they use healthcare.googleapis.com)","Never let untrusted text supply the page URL host — validate the host prefix yourself before invoking","If you front the API with your own proxy, fetch pages through the proxy's own pagination rather than rewriting the host","Contact the library maintainers to extend allowedFHIRHosts if a legitimate Google host is missing"],"exampleFix":"// before (host substituted)\nnext := \"https://internal-proxy.corp/v1/projects/.../fhir/Patient?pageToken=x\"\n// after (host from the original Google response)\nnext := \"https://healthcare.googleapis.com/v1/projects/.../fhir/Patient?pageToken=x\"","handlingStrategy":"validation","validationCode":"function isAllowedFHIRHost(u) { const h = new URL(u).hostname.toLowerCase().replace(/:\\d+$/, ''); return ['healthcare.googleapis.com','healthcare.mtls.googleapis.com'].includes(h); }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only feed page URLs taken directly from Cloud Healthcare API responses back into pagination","Sanitize any LLM-provided URL: parse it and reject if the host is not healthcare.googleapis.com","Never follow pagination links embedded in untrusted document text — they may point at internal hosts"],"tags":["ssrf","security","allowlist","url-validation"],"backgroundTag":"unexpected-host-blocked","analyzedSha":"8cc6e09de2ad7b8bffc77751799585a1401a48eb","analyzedAt":"2026-09-05T01:10:36.887Z","contentChangedAt":"2026-09-05T01:10:36.887Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}