Module: SsrfFilter::Patch::SSLSocket

Defined in:
lib/ssrf_filter/patch/ssl_socket.rb

Class Method Summary collapse

Class Method Details

.apply!Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ssrf_filter/patch/ssl_socket.rb', line 6

def self.apply!
  return if instance_variable_defined?(:@patched_ssl_socket)

  @patched_ssl_socket = true

  ::OpenSSL::SSL::SSLSocket.class_eval do
    # When fetching a url we'd like to have the following workflow:
    # 1) resolve the hostname www.example.com, and choose a public ip address to connect to
    # 2) connect to that specific ip address, to prevent things like DNS TOCTTOU bugs or other trickery
    #
    # Ideally this would happen by the ruby http library giving us control over DNS resolution,
    # but it doesn't. Instead, when making the request we set the uri.hostname to the chosen ip address,
    # and send a 'Host' header of the original hostname, i.e. connect to 'http://93.184.216.34/' and send
    # a 'Host: www.example.com' header.
    #
    # This works for the http case, http://www.example.com. For the https case, this causes certificate
    # validation failures, since the server certificate for https://www.example.com will not have a
    # Subject Alternate Name for 93.184.216.34.
    #
    # Thus we perform the monkey-patch below, modifying SSLSocket's `post_connection_check(hostname)`
    # and `hostname=(hostname)` methods:
    # If our fiber local variable is set, use that for the hostname instead, otherwise behave as usual.
    # The only time the variable will be set is if you are executing inside a `with_forced_hostname` block,
    # which is used in ssrf_filter.rb.
    #
    # An alternative approach could be to pass in our own OpenSSL::X509::Store with a custom
    # `verify_callback` to the ::Net::HTTP.start call, but this would require reimplementing certification
    # validation, which is dangerous. This way we can piggyback on the existing validation and simply pretend
    # that we connected to the desired hostname.

    original_post_connection_check = instance_method(:post_connection_check)
    define_method(:post_connection_check) do |hostname|
      original_post_connection_check.bind(self).call(::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY] ||
        hostname)
    end

    if method_defined?(:hostname=)
      original_hostname = instance_method(:hostname=)
      define_method(:hostname=) do |hostname|
        original_hostname.bind(self).call(::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY] || hostname)
      end
    end

    # This patch is the successor to https://github.com/arkadiyt/ssrf_filter/pull/54
    # Due to some changes in Ruby's net/http library (namely https://github.com/ruby/net-http/pull/36),
    # the SSLSocket in the request was no longer getting `s.hostname` set.
    # The original fix tried to monkey-patch the Regexp class to cause the original code path to execute,
    # but this caused other problems (like https://github.com/arkadiyt/ssrf_filter/issues/61)
    # This fix attempts a different approach to set the hostname on the socket
    original_initialize = instance_method(:initialize)
    define_method(:initialize) do |*args|
      original_initialize.bind(self).call(*args)
      if ::Thread.current.key?(::SsrfFilter::FIBER_HOSTNAME_KEY)
        self.hostname = ::Thread.current[::SsrfFilter::FIBER_HOSTNAME_KEY]
      end
    end
  end
end