{"record":{"id":"89c75c1993290808","repo":"oobabooga/textgen","slug":"invalid-url-userinfo-credentials-in-urls-is-not","errorCode":null,"errorMessage":"Invalid URL: userinfo (credentials) in URLs is not allowed","messagePattern":"Invalid URL: userinfo \\(credentials\\) in URLs is not allowed","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"modules/web_search.py","lineNumber":27,"sourceCode":"from ddgs import DDGS\n\nfrom modules import shared\nfrom modules.logging_colors import logger\n\n\ndef _validate_url(url):\n    \"\"\"Validate that a URL is safe to fetch (not targeting private/internal networks).\"\"\"\n    # Reject characters that cause parsing discrepancies between urlparse and requests,\n    # which can be exploited to bypass SSRF protections (GHSA-27xf-58m5-vxmc).\n    if '\\\\' in url:\n        raise ValueError(\"Invalid URL: backslashes are not allowed\")\n\n    parsed = urlparse(url)\n    if parsed.scheme not in ('http', 'https'):\n        raise ValueError(f\"Unsupported URL scheme: {parsed.scheme}\")\n\n    if '@' in parsed.netloc:\n        raise ValueError(\"Invalid URL: userinfo (credentials) in URLs is not allowed\")\n\n    hostname = parsed.hostname\n    if not hostname:\n        raise ValueError(\"No hostname in URL\")\n\n    # Resolve hostname and check all returned addresses\n    try:\n        for family, _, _, _, sockaddr in socket.getaddrinfo(hostname, None):\n            ip = ipaddress.ip_address(sockaddr[0])\n            if not ip.is_global:\n                raise ValueError(f\"Access to non-public address {ip} is blocked\")\n    except socket.gaierror:\n        raise ValueError(f\"Could not resolve hostname: {hostname}\")\n\n\ndef safe_get(url, headers=None, timeout=10, max_redirects=5):\n    \"\"\"Fetch a URL with SSRF-safe redirect handling. Validates every hop.\"\"\"\n    _validate_url(url)","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/oobabooga/textgen/blob/ed888c71f221df552750e1834b3654abab8ae345/modules/web_search.py#L9-L45","documentation":"Raised by _validate_url() when the URL's network location contains an '@', i.e. embedded userinfo ('user:pass@host'). Different URL parsers and HTTP clients disagree about where the host begins when userinfo is present, which has been used to bypass SSRF host checks (the validator sees one host, the fetcher connects to another). It is rejected unconditionally, even for userinfo without a password.","triggerScenarios":"Fetching 'https://user:pass@example.com/data', 'https://token@example.com', or any URL where the netloc contains '@'. Also triggered by a redirect Location containing embedded credentials, which is a classic SSRF-bypass attempt.","commonSituations":"User pastes an authenticated URL copied from a browser or API doc (basic-auth style); scripts that embed API tokens in the URL; malicious redirect chains trying to smuggle a private host past validation.","solutions":["Remove the userinfo and pass credentials via headers instead (e.g. Authorization header), then re-submit the clean URL.","If credentials are genuinely needed, strip them from the URL and configure HTTP Basic auth at the request layer outside the guarded fetch path.","Treat this error on a redirect hop as a security signal — do not retry; report the offending upstream URL."],"exampleFix":"# before\nresp = safe_get('https://user:pass@example.com/api')  # raises ValueError\n\n# after\nfrom urllib.parse import urlsplit, urlunsplit\nparts = urlsplit('https://user:pass@example.com/api')\nclean = urlunsplit((parts.scheme, parts.netloc.rpartition('@')[2], parts.path, parts.query, ''))\nresp = safe_get(clean, headers={'Authorization': 'Basic <b64 user:pass>'})","handlingStrategy":"validation","validationCode":"from urllib.parse import urlsplit, urlunsplit\n\ndef strip_userinfo(url: str) -> str:\n    parts = urlsplit(url)\n    if '@' in parts.netloc:\n        host = parts.netloc.rpartition('@')[2]\n        parts = parts._replace(netloc=host)\n    return urlunsplit(parts)\n\nurl = strip_userinfo(url)  # then pass credentials via headers","typeGuard":null,"tryCatchPattern":"try:\n    resp = safe_get(url)\nexcept ValueError as e:\n    if 'userinfo' in str(e):\n        raise RuntimeError('Embedded credentials are not supported; move them to an Authorization header') from e\n    raise","preventionTips":["Never embed credentials in URLs; use Authorization headers.","Sanitize copied-from-browser URLs (they often carry encoded userinfo) before programmatic use.","Treat userinfo in redirect Location headers as an SSRF-bypass attempt, not a feature."],"tags":["ssrf","url-validation","security","authentication"],"backgroundTag":null,"analyzedSha":"ed888c71f221df552750e1834b3654abab8ae345","analyzedAt":"2026-08-15T05:24:21.000Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}