{"record":{"id":"d1f91108ba1847f8","repo":"jnunemaker/httparty","slug":"requested-uri-new-uri-has-host-new-uri-hos","errorCode":null,"errorMessage":"Requested URI '#{new_uri}' has host '#{new_uri.host}' but the configured base_uri '#{normalized_base}' has host '#{normalized_base.host}'. This request could send credentials to an unintended server.","messagePattern":"Requested URI '#(.+?)' has host '#(.+?)' but the configured base_uri '#(.+?)' has host '#(.+?)'\\. This request could send credentials to an unintended server\\.","errorType":"exception","errorClass":"HTTParty::UnsafeURIError","httpStatus":null,"severity":"error","filePath":"lib/httparty/request.rb","lineNumber":460,"sourceCode":"        text,\n        content_type: content_type,\n        assume_utf16_is_big_endian: assume_utf16_is_big_endian\n      ).call\n    end\n\n    def validate_uri_safety!(new_uri)\n      return if options[:skip_uri_validation]\n\n      configured_base_uri = options[:base_uri]\n      return unless configured_base_uri\n\n      normalized_base = options[:uri_adapter].parse(\n        HTTParty.normalize_base_uri(configured_base_uri)\n      )\n\n      return if new_uri.host == normalized_base.host\n\n      raise UnsafeURIError,\n        \"Requested URI '#{new_uri}' has host '#{new_uri.host}' but the \" \\\n        \"configured base_uri '#{normalized_base}' has host '#{normalized_base.host}'. \" \\\n        \"This request could send credentials to an unintended server.\"\n    end\n  end\nend\n","sourceCodeStart":442,"sourceCodeEnd":467,"githubUrl":"https://github.com/jnunemaker/httparty/blob/8f4a09e343b94de9f934f388028ca97620c9b378/lib/httparty/request.rb#L442-L467","documentation":"HTTParty raises UnsafeURIError from validate_uri_safety! when a request uses an absolute URI whose host differs from the configured base_uri host. This is an SSRF guard: if a class pins base_uri (typically alongside basic_auth/digest_auth or headers with secrets), a full URL pointing at another host would silently send those credentials elsewhere, so httparty aborts. The check is skipped for redirects and when options[:skip_uri_validation] is set, and only runs when options[:base_uri] is present.","triggerScenarios":"`class Api; include HTTParty; base_uri 'https://api.example.com'; basic_auth 'u','p'; end` then `Api.get('https://other-host.io/resource')`; or dynamic absolute URLs read from a field that no longer matches the pinned base host (e.g. environment mismatch where base_uri points at staging but the URL is production).","commonSituations":"API clients that pin base_uri for safety but sometimes receive absolute URLs (pagination next links, webhooks, entity hrefs), multi-environment configs where base_uri and the passed URL drift, and refactors that add base_uri to an existing class already passing full URLs.","solutions":["Keep hosts consistent: request relative paths against the pinned base_uri (`Api.get('/resource')`).","If the different host is intentional and trusted, move that call to a separate HTTParty class with the correct base_uri (or none).","Only for vetted URLs, bypass explicitly per request: `Api.get(url, skip_uri_validation: true)` — this re-enables credential leakage risk, so scope it narrowly.","When consuming pagination 'next' links, parse them and re-issue relative paths instead of following absolute URLs."],"exampleFix":"# before\nclass Api\n  include HTTParty\n  base_uri 'https://api.example.com'\n  basic_auth 'user', 'pass'\nend\nApi.get('https://cdn.example.net/file')   # UnsafeURIError\n\n# after (relative path against pinned host)\nApi.get('/file')\n\n# after (intentional different host: separate class, no shared creds)\nclass Cdn\n  include HTTParty\nend\nCdn.get('https://cdn.example.net/file')","handlingStrategy":"try-catch","validationCode":"base = URI.parse(HTTParty.normalize_base_uri(base_uri_string))\nraise HTTParty::UnsafeURIError, \"#{uri.host} != pinned #{base.host}\" if uri.host && uri.host != base.host","typeGuard":"host_matches_base = ->(url, base) do\n  URI.parse(url.to_s).host == URI.parse(HTTParty.normalize_base_uri(base)).host\nend","tryCatchPattern":"begin\n  Api.get(url)\nrescue HTTParty::UnsafeURIError => e\n  Rails.logger.warn(\"host mismatch blocked: #{e.message}\")\n  OtherClient.get(url)  # separate class without shared credentials\nend","preventionTips":["Request relative paths whenever base_uri is pinned.","Create one HTTParty class per host instead of one class with bypasses.","If you must follow absolute pagination/next links, either strip credentials or consciously pass skip_uri_validation: true for that single vetted call.","Never enable skip_uri_validation for user-supplied URLs."],"tags":["ruby","httparty","ssrf","security","uri","base-uri","credentials"],"backgroundTag":"ssrf-protection","analyzedSha":"8f4a09e343b94de9f934f388028ca97620c9b378","analyzedAt":"2026-08-21T19:30:42.003Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}