{"record":{"id":"073b849ed18dc765","repo":"iflytek/astron-agent","slug":"outbound-url-contains-control-characters","errorCode":null,"errorMessage":"Outbound URL contains control characters","messagePattern":"Outbound URL contains control characters","errorType":"exception","errorClass":"OutboundPolicyError","httpStatus":null,"severity":"error","filePath":"core/plugin/link/infra/tool_exector/ssrf_guard.py","lineNumber":197,"sourceCode":"    return scheme, hostname, normalized_port\n\n\ndef _parse_http_url(url: str) -> SplitResult:\n    if not isinstance(url, str):\n        raise OutboundPolicyError(\"Outbound URL is malformed\")\n    _validate_url_characters(url)\n    try:\n        parsed = urlsplit(url)\n        port = parsed.port\n    except (TypeError, ValueError) as exc:\n        raise OutboundPolicyError(\"Outbound URL is malformed\") from exc\n    _validate_parsed_http_url(parsed, port)\n    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, ...]:","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/core/plugin/link/infra/tool_exector/ssrf_guard.py#L179-L215","documentation":"This SSRF guard rejects any outbound tool URL containing ASCII control characters (bytes < 0x20, including tab/newline/CR/NUL, or DEL 0x7F). Control characters in URLs can enable CRLF header injection or parser confusion where the validated URL differs from the one actually requested, so the guard fails fast before any request.","triggerScenarios":"Calling _parse_http_url (via the plugin's outbound HTTP execution path) with a URL containing \\n, \\r, \\t, \\0 or any char with ord < 0x20 or == 0x7F — typically URLs assembled from untrusted tool input, pasted multi-line text, or template joins with stray newlines.","commonSituations":"URL pasted from a chat message with a trailing newline; f-string/template join introducing a line break; tab-separated values inside a query param; deliberate CRLF injection attempts from untrusted callers.","solutions":["Sanitize the string: url.strip() then remove control chars, e.g. ''.join(c for c in url if ord(c) >= 0x20 and ord(c) != 0x7F).","Percent-encode dynamic values with urllib.parse.quote before building the URL.","Log repr(url) to locate the hidden control character and fix the upstream producer.","If the URL comes from user/tool input, validate and reject it upstream with a clear message instead of relying on the guard."],"exampleFix":"// before\nurl = f\"https://api.example.com/{user_input}\\n\"\nclient.get(url)\n// after\nurl = f\"https://api.example.com/{urllib.parse.quote(user_input, safe='')}\".strip()\nclient.get(url)","handlingStrategy":"validation","validationCode":"def is_safe_url(url: str) -> bool:\n    return isinstance(url, str) and all(ord(c) >= 0x20 and ord(c) != 0x7F for c in url)","typeGuard":null,"tryCatchPattern":"from ssrf_guard import OutboundPolicyError\ntry:\n    client.get(url)\nexcept OutboundPolicyError as e:\n    logger.warning(\"rejected outbound URL %r: %s\", url, e)","preventionTips":["Always strip and percent-encode user-supplied URL components with urllib.parse.quote","Never interpolate raw tool/chat input into URLs","Use repr() when logging URLs to expose hidden control characters"],"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"}