{"record":{"id":"0177fec856d59c0b","repo":"Panniantong/Agent-Reach","slug":"only-the-v2ex-https-api-is-allowed","errorCode":null,"errorMessage":"only the V2EX HTTPS API is allowed","messagePattern":"only the V2EX HTTPS API is allowed","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent_reach/channels/v2ex.py","lineNumber":43,"sourceCode":"    return f\"{_API_BASE}{path}?{urlencode(params)}\"\n\n\ndef _validate_api_url(url: str) -> None:\n    \"\"\"Allow only the public V2EX HTTPS JSON API.\"\"\"\n    try:\n        parsed = urlsplit(url)\n        port = parsed.port\n    except ValueError as exc:\n        raise ValueError(\"invalid V2EX API URL\") from exc\n    if (\n        parsed.scheme.lower() != \"https\"\n        or (parsed.hostname or \"\").lower() not in {\"v2ex.com\", \"www.v2ex.com\"}\n        or port not in {None, 443}\n        or parsed.username is not None\n        or parsed.password is not None\n        or not parsed.path.startswith(\"/api/\")\n    ):\n        raise ValueError(\"only the V2EX HTTPS API is allowed\")\n\n\ndef _get_json_with_urllib(url: str) -> Any:\n    \"\"\"Fetch JSON with Python's standard HTTP stack.\"\"\"\n    _validate_api_url(url)\n    req = urllib.request.Request(url, headers={\"User-Agent\": _UA})\n    with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:\n        raw = resp.read(_MAX_RESPONSE_BYTES + 1)\n    if len(raw) > _MAX_RESPONSE_BYTES:\n        raise ValueError(\"V2EX API response exceeds the 1 MiB safety limit\")\n    return json.loads(raw.decode(\"utf-8\"))\n\n\ndef _is_unexpected_tls_eof(error: BaseException) -> bool:\n    \"\"\"Return whether an exception chain contains the retryable TLS EOF.\"\"\"\n    pending: list[BaseException] = [error]\n    seen: set[int] = set()\n    while pending:","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/Panniantong/Agent-Reach/blob/93ae1d18c37b707dec053c7c4f9d91cd8ef8943d/agent_reach/channels/v2ex.py#L25-L61","documentation":"Raised by _validate_api_url() when the URL parses fine but violates the V2EX channel's allowlist: scheme must be https, host must be v2ex.com or www.v2ex.com, port only None/443, no userinfo (user:pass@), and the path must start with /api/. This SSRF guard ensures only the public V2EX JSON API is fetched, never arbitrary hosts.","triggerScenarios":"Calling _get_json_with_urllib() with http:// scheme, a different host (api.example.com), an odd port (https://v2ex.com:8443/...), embedded credentials (https://user:pass@v2ex.com/...), or a non-/api/ path (https://www.v2ex.com/t/123).","commonSituations":"Attempting to proxy the channel to a V2EX mirror or self-hosted instance; feeding a topic page URL (no /api/ prefix) instead of an API endpoint; adding a token as userinfo; trying http to dodge TLS issues behind a proxy.","solutions":["Use only https://www.v2ex.com/api/... endpoints (or v2ex.com), no port, no credentials","Convert page URLs to API URLs (e.g. topic page /t/123 → /api/topics/show.json?id=123)","For mirrors/proxies you cannot use this channel — the allowlist is by design; call the mirror yourself outside Agent Reach","If a proxy is required, configure it via HTTPS_PROXY env (urllib honors it) instead of rewriting the URL"],"exampleFix":"# before\n _get_json_with_urllib(\"https://api.v2ex.com/topics/show.json?id=1\")\n _get_json_with_urllib(\"http://www.v2ex.com/api/topics/show.json?id=1\")\n# after\n _get_json_with_urllib(\"https://www.v2ex.com/api/topics/show.json?id=1\")","handlingStrategy":"validation","validationCode":"from urllib.parse import urlsplit\n\nALLOWED_HOSTS = {\"v2ex.com\", \"www.v2ex.com\"}\n\ndef url_on_v2ex_api(url: str) -> bool:\n    try:\n        p = urlsplit(url)\n        port = p.port\n    except ValueError:\n        return False\n    return (\n        p.scheme.lower() == \"https\"\n        and (p.hostname or \"\").lower() in ALLOWED_HOSTS\n        and port in (None, 443)\n        and p.username is None and p.password is None\n        and p.path.startswith(\"/api/\")\n    )","typeGuard":"def is_v2ex_api_url(url) -> bool:\n    if not isinstance(url, str):\n        return False\n    return url_on_v2ex_api(url)","tryCatchPattern":"if not url_on_v2ex_api(url):\n    raise InvalidInput(url)\ntry:\n    _get_json_with_urllib(url)\nexcept ValueError as exc:\n    if \"only the V2EX HTTPS API is allowed\" in str(exc):\n        map_page_url_to_api(url)  # e.g. /t/123 -> /api/topics/show.json?id=123","preventionTips":["Treat the allowlist as an SSRF contract; don't try to bypass it","Translate page URLs to /api/ endpoints before calling the channel","Use HTTPS_PROXY for network routing instead of URL rewriting"],"tags":["v2ex","ssrf","url-validation","network","security"],"backgroundTag":null,"analyzedSha":"93ae1d18c37b707dec053c7c4f9d91cd8ef8943d","analyzedAt":"2026-08-14T22:54:06.735Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}