diff --git a/lib/falcon/middleware/proxy.rb b/lib/falcon/middleware/proxy.rb index aafd1a70..8bc185d2 100644 --- a/lib/falcon/middleware/proxy.rb +++ b/lib/falcon/middleware/proxy.rb @@ -33,6 +33,12 @@ class Proxy < Protocol::HTTP::Middleware FORWARDED = "forwarded" X_FORWARDED_FOR = "x-forwarded-for" X_FORWARDED_PROTO = "x-forwarded-proto" + X_FORWARDED_HOST = "x-forwarded-host" + X_FORWARDED_PORT = "x-forwarded-port" + X_FORWARDED_SCHEME = "x-forwarded-scheme" + X_FORWARDED_SSL = "x-forwarded-ssl" + X_REAL_IP = "x-real-ip" + CLIENT_IP = "client-ip" VIA = "via" CONNECTION = "connection" @@ -48,6 +54,19 @@ class Proxy < Protocol::HTTP::Middleware "upgrade", ] + # Headers which disclose client/proxy provenance across a trust boundary. + FORWARDED_HEADERS = [ + FORWARDED, + X_FORWARDED_FOR, + X_FORWARDED_PROTO, + X_FORWARDED_HOST, + X_FORWARDED_PORT, + X_FORWARDED_SCHEME, + X_FORWARDED_SSL, + X_REAL_IP, + CLIENT_IP, + ] + # Initialize the proxy middleware. # @parameter app [Protocol::HTTP::Middleware] The middleware to use if a request can't be proxied. # @parameter hosts [Hash(String, Service::Proxy)] The host applications to proxy to. @@ -99,6 +118,12 @@ def prepare_headers(headers) headers.extract(HOP_HEADERS) end + # Prepare forwarded headers before adding the proxy's own provenance. + # Override this method to preserve or transform trusted upstream headers. + def prepare_forwarded_headers(headers) + headers.extract(FORWARDED_HEADERS) + end + # Prepare the request to be proxied to the specified host. # In particular, we set appropriate {VIA}, {FORWARDED}, {X_FORWARDED_FOR} and {X_FORWARDED_PROTO} headers. def prepare_request(request, host) @@ -114,6 +139,8 @@ def prepare_request(request, host) # The authority of the request must match the authority of the endpoint we are proxying to, otherwise SNI and other things won't work correctly. request.authority = host.authority + self.prepare_forwarded_headers(request.headers) + if address = request.remote_address request.headers.add(X_FORWARDED_FOR, address.ip_address) forwarded << "for=#{address.ip_address}" diff --git a/test/falcon/middleware/proxy.rb b/test/falcon/middleware/proxy.rb index 2444ffc3..a19c5a31 100644 --- a/test/falcon/middleware/proxy.rb +++ b/test/falcon/middleware/proxy.rb @@ -93,4 +93,57 @@ def proxy_for(**options) message: be(:include?, "Request authority: www.google.com"), ) end + + it "strips forwarded headers before preparing requests" do + headers = Protocol::HTTP::Headers[ + "forwarded" => "for=203.0.113.1;proto=http", + "x-forwarded-for" => "203.0.113.1", + "x-forwarded-proto" => "http", + "x-forwarded-host" => "example.com", + "x-forwarded-port" => "80", + "x-forwarded-scheme" => "http", + "x-forwarded-ssl" => "off", + "x-real-ip" => "203.0.113.1", + "client-ip" => "203.0.113.1", + ] + request = Protocol::HTTP::Request.new("https", "www.google.com", "GET", "/", "HTTP/1.1", headers, nil) + host = proxy_for(authority: "www.google.com", endpoint: Async::HTTP::Endpoint.parse("https://www.google.com")) + + expect(request).to receive(:remote_address).and_return(Addrinfo.ip("127.0.0.1")) + + proxy.prepare_request(request, host) + + expect(request.headers["forwarded"]).to be == ["for=127.0.0.1;proto=https"] + expect(request.headers["x-forwarded-for"]).to be == ["127.0.0.1"] + expect(request.headers["x-forwarded-proto"]).to be == ["https"] + expect(request.headers["x-forwarded-host"]).to be_nil + expect(request.headers["x-forwarded-port"]).to be_nil + expect(request.headers["x-forwarded-scheme"]).to be_nil + expect(request.headers["x-forwarded-ssl"]).to be_nil + expect(request.headers["x-real-ip"]).to be_nil + expect(request.headers["client-ip"]).to be_nil + end + + it "allows forwarded header preparation to be overridden" do + klass = Class.new(subject) do + def prepare_forwarded_headers(headers) + end + end + proxy = klass.new(Falcon::Middleware::BadRequest, {}) + headers = Protocol::HTTP::Headers[ + "forwarded" => "for=203.0.113.1;proto=http", + "x-forwarded-for" => "203.0.113.1", + "x-forwarded-proto" => "http", + ] + request = Protocol::HTTP::Request.new("https", "www.google.com", "GET", "/", "HTTP/1.1", headers, nil) + host = proxy_for(authority: "www.google.com", endpoint: Async::HTTP::Endpoint.parse("https://www.google.com")) + + expect(request).to receive(:remote_address).and_return(Addrinfo.ip("127.0.0.1")) + + proxy.prepare_request(request, host) + + expect(request.headers["forwarded"]).to be == ["for=203.0.113.1;proto=http", "for=127.0.0.1;proto=https"] + expect(request.headers["x-forwarded-for"]).to be == ["203.0.113.1", "127.0.0.1"] + expect(request.headers["x-forwarded-proto"]).to be == ["http", "https"] + end end