{"record":{"id":"c54c33fd61c3b8aa","repo":"iflytek/astron-agent","slug":"outbound-url-must-not-include-a-fragment","errorCode":null,"errorMessage":"Outbound URL must not include a fragment","messagePattern":"Outbound URL must not include a fragment","errorType":"exception","errorClass":"OutboundPolicyError","httpStatus":null,"severity":"error","filePath":"core/plugin/link/infra/tool_exector/ssrf_guard.py","lineNumber":210,"sourceCode":"    return parsed\n\n\ndef _validate_url_characters(url: str) -> None:\n    if any(ord(character) < 0x20 or ord(character) == 0x7F for character in url):\n        raise OutboundPolicyError(\"Outbound URL contains control characters\")\n\n\ndef _validate_parsed_http_url(parsed: SplitResult, port: Union[int, None]) -> None:\n    if parsed.scheme.lower() not in _ALLOWED_SCHEMES:\n        raise OutboundPolicyError(\"Only HTTP and HTTPS tool URLs are allowed\")\n    if not parsed.hostname:\n        raise OutboundPolicyError(\"Outbound URL must include a hostname\")\n    if parsed.username is not None or parsed.password is not None:\n        raise OutboundPolicyError(\"Outbound URL must not include user information\")\n    if \"\\\\\" in parsed.netloc:\n        raise OutboundPolicyError(\"Outbound URL authority is invalid\")\n    if parsed.fragment:\n        raise OutboundPolicyError(\"Outbound URL must not include a fragment\")\n    if port is not None and not 1 <= port <= 65535:\n        raise OutboundPolicyError(\"Outbound URL port is invalid\")\n\n\ndef _parse_networks(raw_value: str, setting_name: str) -> Tuple[IpNetwork, ...]:\n    networks = []\n    for entry in raw_value.split(\",\"):\n        value = entry.strip()\n        if not value:\n            continue\n        try:\n            networks.append(ipaddress.ip_network(value, strict=False))\n        except ValueError as exc:\n            raise OutboundPolicyError(f\"Invalid {setting_name} entry\") from exc\n    return tuple(networks)\n\n\ndef _parse_domains(raw_value: str) -> Tuple[str, ...]:","sourceCodeStart":192,"sourceCodeEnd":228,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/core/plugin/link/infra/tool_exector/ssrf_guard.py#L192-L228","documentation":"URL fragments (#anchor) are forbidden in outbound tool URLs. Fragments are client-side only and not sent to the server per spec, but HTTP clients and intermediate parsers handle them inconsistently, so the guard rejects them outright to eliminate the ambiguity.","triggerScenarios":"_parse_http_url receives a URL with a '#...' suffix — e.g. a link copied from a browser address bar including an anchor, or a template where an unescaped '#' appears in a path/query value.","commonSituations":"Copying https://site/docs#section into tool config; unencoded '#' inside a query value; SPA hash routes.","solutions":["Remove the #fragment portion from the URL.","If '#' is part of a data value, percent-encode it: urllib.parse.quote('#') → %23.","Strip programmatically: url.split('#', 1)[0].","Fix the copy/paste or template step that introduced the anchor."],"exampleFix":"// before\nurl = \"https://api.example.com/items#section-2\"\n// after\nurl = \"https://api.example.com/items\".split('#', 1)[0]","handlingStrategy":"validation","validationCode":"def has_no_fragment(url: str) -> bool:\n    return not urlsplit(url).fragment","typeGuard":null,"tryCatchPattern":"try:\n    client.get(url)\nexcept OutboundPolicyError:\n    url = url.split('#', 1)[0]\n    client.get(url)","preventionTips":["Strip '#' fragments when copying URLs from browsers into config","Percent-encode '#' inside data values with urllib.parse.quote","Keep fragments client-side only; server APIs should never need them"],"tags":["security","ssrf","url-validation"],"backgroundTag":"invalid-url-format","analyzedSha":"5e758547a83371a5a4b29dadf4ac03e8dd527635","analyzedAt":"2026-09-12T08:03:51.356Z","contentChangedAt":"2026-09-12T08:03:51.356Z","schemaVersion":2},"datasetVersion":"2026-09-19T12:17:13.211Z"}