{"record":{"id":"15b0ce8de9456e33","repo":"iflytek/astron-agent","slug":"private-endpoint-allow-list-entries-must-not-include-params","errorCode":null,"errorMessage":"PRIVATE_ENDPOINT_ALLOW_LIST entries must not include params or a query","messagePattern":"PRIVATE_ENDPOINT_ALLOW_LIST entries must not include params or a query","errorType":"validation","errorClass":"OutboundPolicyError","httpStatus":null,"severity":"error","filePath":"core/plugin/link/infra/tool_exector/ssrf_guard.py","lineNumber":263,"sourceCode":"        except OutboundPolicyError as exc:\n            raise OutboundPolicyError(\"Invalid DOMAIN_BLACK_LIST entry\") from exc\n    return tuple(domains)\n\n\ndef _parse_private_endpoints(raw_value: str) -> Tuple[Endpoint, ...]:\n    endpoints = []\n    for entry in raw_value.split(\",\"):\n        value = entry.strip()\n        if not value:\n            continue\n        try:\n            parsed = _parse_http_url(value)\n        except OutboundPolicyError as exc:\n            raise OutboundPolicyError(\n                \"Invalid PRIVATE_ENDPOINT_ALLOW_LIST entry\"\n            ) from exc\n        if parsed.query or \";\" in parsed.path:\n            raise OutboundPolicyError(\n                \"PRIVATE_ENDPOINT_ALLOW_LIST entries must not include params or a query\"\n            )\n        endpoints.append(_endpoint(parsed))\n    return tuple(endpoints)\n\n\ndef _endpoint(parsed: SplitResult) -> Endpoint:\n    scheme, hostname, port = _origin(parsed.geturl())\n    return scheme, hostname, port, parsed.path or \"/\"\n\n\ndef _normalize_hostname(hostname: str) -> str:\n    value = hostname.strip().lower().rstrip(\".\")\n    if _parse_ip(value) is not None:\n        return value\n    try:\n        normalized = URL.build(scheme=\"http\", host=value).raw_host\n    except (TypeError, ValueError, UnicodeError) as exc:","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/core/plugin/link/infra/tool_exector/ssrf_guard.py#L245-L281","documentation":"OutboundPolicyError raised when a PRIVATE_ENDPOINT_ALLOW_LIST entry parses as a URL but contains a query string or matrix/semicolon params in the path. The allow list defines endpoint origins, so params/query are meaningless and could be used to smuggle past matching; the guard rejects them at startup.","triggerScenarios":"from_environment() -> _parse_private_endpoints() parses an entry where parsed.query is non-empty or the path contains ';' — e.g. 'https://api.corp/path;v=2' or 'https://db.internal/?pool=main'.","commonSituations":"Developers paste a full working URL (with query params they used in a browser/curl) into the allow list instead of just the origin/base URL.","solutions":["Strip the query string and any ';params' from each entry, keeping only scheme://host[:port]/path.","If per-endpoint parameters are needed, pass them where the request is made, not in the allow-list entry.","Add a startup config lint/test that asserts no '?' or ';' appears in allow-list entries before deploy."],"exampleFix":"// before\nPRIVATE_ENDPOINT_ALLOW_LIST=https://db.internal/?pool=main\n// after\nPRIVATE_ENDPOINT_ALLOW_LIST=https://db.internal/","handlingStrategy":"validation","validationCode":"def clean_entry(entry: str) -> str:\n    from yarl import URL\n    u = URL(entry)\n    return str(u.with_query(None)).split(\";\")[0]\nassert not any(c in e for e in entries for c in \"?;\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Store only origin/base URLs in the allow list","Strip query strings automatically in deployment tooling","Add CI lint that scans env files for '?'/';' in allow-list values"],"tags":["ssrf","configuration","env-var","url-parsing"],"backgroundTag":"invalid-config-value","analyzedSha":"5e758547a83371a5a4b29dadf4ac03e8dd527635","analyzedAt":"2026-09-12T08:03:51.356Z","contentChangedAt":"2026-09-12T08:03:51.356Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}