iflytek/astron-agent · error · OutboundPolicyError
Outbound URL contains control characters
Error message
Outbound URL contains control characters
What it means
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.
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.
Example fix
// before
url = f"https://api.example.com/{user_input}\n"
client.get(url)
// after
url = f"https://api.example.com/{urllib.parse.quote(user_input, safe='')}".strip()
client.get(url) Defensive patterns
Strategy: validation
Validate before calling
def is_safe_url(url: str) -> bool:
return isinstance(url, str) and all(ord(c) >= 0x20 and ord(c) != 0x7F for c in url) Try / catch
from ssrf_guard import OutboundPolicyError
try:
client.get(url)
except OutboundPolicyError as e:
logger.warning("rejected outbound URL %r: %s", url, e) Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- HTTPClientError
- MODEL_URL_CHECK_FAILED
- MODEL_URL_CHECK_FAILED
- MODEL_URL_ILLEGAL_FAILED
- Only HTTP and HTTPS tool URLs are allowed
AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12).
Data as JSON: /api/errors/073b849ed18dc765.
Report an issue: GitHub.
Appendix: source
Thrown at core/plugin/link/infra/tool_exector/ssrf_guard.py:197
return scheme, hostname, normalized_port
def _parse_http_url(url: str) -> SplitResult:
if not isinstance(url, str):
raise OutboundPolicyError("Outbound URL is malformed")
_validate_url_characters(url)
try:
parsed = urlsplit(url)
port = parsed.port
except (TypeError, ValueError) as exc:
raise OutboundPolicyError("Outbound URL is malformed") from exc
_validate_parsed_http_url(parsed, port)
return parsed
def _validate_url_characters(url: str) -> None:
if any(ord(character) < 0x20 or ord(character) == 0x7F for character in url):
raise OutboundPolicyError("Outbound URL contains control characters")
def _validate_parsed_http_url(parsed: SplitResult, port: Union[int, None]) -> None:
if parsed.scheme.lower() not in _ALLOWED_SCHEMES:
raise OutboundPolicyError("Only HTTP and HTTPS tool URLs are allowed")
if not parsed.hostname:
raise OutboundPolicyError("Outbound URL must include a hostname")
if parsed.username is not None or parsed.password is not None:
raise OutboundPolicyError("Outbound URL must not include user information")
if "\\" in parsed.netloc:
raise OutboundPolicyError("Outbound URL authority is invalid")
if parsed.fragment:
raise OutboundPolicyError("Outbound URL must not include a fragment")
if port is not None and not 1 <= port <= 65535:
raise OutboundPolicyError("Outbound URL port is invalid")
def _parse_networks(raw_value: str, setting_name: str) -> Tuple[IpNetwork, ...]:View on GitHub (pinned to 5e758547a8)