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

  1. 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).
  2. Percent-encode dynamic values with urllib.parse.quote before building the URL.
  3. Log repr(url) to locate the hidden control character and fix the upstream producer.
  4. 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

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


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)