Files
once-campfire/test/models/opengraph/fetch_test.rb
T
Rosa GutierrezandClaude Opus 5.5 a7e9ea53dd Don't let an http_proxy environment variable bypass the pinned address in link previews
Opengraph::Fetch connects to the IP it checked, not to a fresh lookup of
the host. With http_proxy set, Net::HTTP would instead send the hostname
to the proxy, which looks it up again, so the connection could end up
somewhere other than the checked address. Pass nil as the proxy address,
as web push delivery already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:45:43 +02:00

136 lines
5.4 KiB
Ruby

require "test_helper"
require "restricted_http/private_network_guard"
class Opengraph::FetchTest < ActiveSupport::TestCase
setup do
@fetch = Opengraph::Fetch.new
@url = URI.parse("https://www.example.com")
end
test "#fetch_document fetches valid HTML" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
assert_equal "<body>ok<body>", @fetch.fetch_document(@url)
end
test "#fetch_document discards other content types" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "I'm not HTML!", headers: { content_type: "text/plain" })
assert_nil @fetch.fetch_document(@url)
end
test "#fetch_document follows redirects" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 302, headers: { location: "https://www.other.com/" })
WebMock.stub_request(:get, "https://www.other.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
assert_equal "<body>ok<body>", @fetch.fetch_document(@url)
end
test "#fetch_document does not follow redirects to private networks" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 302, headers: { location: "https://www.other.com/" })
WebMock.stub_request(:get, "https://www.other.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
Resolv.stubs(:getaddresses).with("www.other.com").returns([ "127.0.0.1" ])
assert_raises RestrictedHTTP::Violation do
@fetch.fetch_document(@url, ip: "1.2.3.4")
end
end
test "#fetch_document resolves hostnames once to avoid DNS rebinding" do
# Allow but interrupt a real connection to demonstrate that we connect
# to a resolved IP, not a hostname to re-resolve.
WebMock.disable_net_connect! allow: [ @url.host ]
Resolv.stubs(:getaddresses).with(@url.host).returns([ "1.2.3.4" ], [ "127.0.0.1" ])
TCPSocket.expects(:open).with { |*args, **| args.first == @url.host }.never
TCPSocket.expects(:open).with { |*args, **| args.first == "1.2.3.4" && args[1] == 443 }.throws(:dns_not_rebound)
assert_throws :dns_not_rebound do
@fetch.fetch_document(@url)
end
end
test "#fetch_document resolves redirect location hostnames once to avoid DNS rebinding" do
# Stub the initial URL to redirect to a DNS-rebound location
WebMock.stub_request(:get, "https://www.other.com/")
.to_return(status: 302, headers: { location: @url.to_s })
# Allow but interrupt a real connection to demonstrate that we connect
# to a resolved IP, not a hostname to re-resolve.
WebMock.disable_net_connect! allow: [ @url.host ]
Resolv.stubs(:getaddresses).with(@url.host).returns([ "1.2.3.4" ], [ "127.0.0.1" ])
TCPSocket.expects(:open).with { |*args, **| args.first == @url.host }.never
TCPSocket.expects(:open).with { |*args, **| args.first == "1.2.3.4" && args[1] == 443 }.throws(:dns_not_rebound)
assert_throws :dns_not_rebound do
@fetch.fetch_document(URI.parse("https://www.other.com/"), ip: "1.2.3.4")
end
end
test "#fetch_document connects to the resolved IP even when a proxy is configured" do
url = URI.parse("http://www.example.com/")
saved = ENV.slice("http_proxy", "HTTP_PROXY")
%w[ http_proxy HTTP_PROXY ].each { |k| ENV[k] = "http://proxy.internal:3128" }
WebMock.disable_net_connect! allow: [ url.host ]
TCPSocket.expects(:open).with { |*args, **| args.first == "proxy.internal" }.never
TCPSocket.expects(:open).with { |*args, **| args.first == "1.2.3.4" && args[1] == 80 }.throws(:not_proxied)
assert_throws :not_proxied do
@fetch.fetch_document(url, ip: "1.2.3.4")
end
ensure
%w[ http_proxy HTTP_PROXY ].each { |k| ENV.delete(k) }
saved.each { |k, v| ENV[k] = v }
end
test "#fetch_document is empty following redirects that never finish" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 302, headers: { location: "https://www.example.com/" })
assert_raises Opengraph::Fetch::TooManyRedirectsError do
@fetch.fetch_document(@url)
end
end
test "#fetch_document ignores large responses" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "too large", headers: { content_length: 1.gigabyte, content_type: "text/html" })
assert_nil @fetch.fetch_document(@url)
end
test "#fetch_document ignores large responses that were missing their content length" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: large_body_content, headers: { content_type: "text/html" })
assert_nil @fetch.fetch_document(@url)
end
test "#fetch_document ignores large responses that were lying about their content length" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: large_body_content, headers: { content_length: 1.megabyte, content_type: "text/html" })
assert_nil @fetch.fetch_document(@url)
end
test "fetch content type" do
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
url = URI.parse("https://example.com/image.png")
assert_equal "image/png", @fetch.fetch_content_type(url)
end
private
def large_body_content
"x" * (Opengraph::Fetch::MAX_BODY_SIZE + 1)
end
end