class Arachni::Browser::Javascript
Provides access to the {Browser}‘s JavaScript environment, mainly helps group and organize functionality related to our custom Javascript
interfaces.
@author Tasos “Zapotek” Laskos <tasos.laskos@arachni-scanner.com>
Constants
- EACH_DOM_ELEMENT_WITH_EVENTS_BATCH_SIZE
- EVENTS
- NO_EVENTS_FOR_ELEMENTS
- SCRIPT_BASE_URL
@return [String]
URL to use when requesting our custom JS scripts.
- SCRIPT_LIBRARY
@return [String]
Filesystem directory containing the JS scripts.
- SCRIPT_SOURCES
- TOKEN
Attributes
@return [String]
Inject custom JS code right after the initialization of the custom JS interfaces.
@return [DOMMonitor]
{Proxy} for the `DOMMonitor` JS interface.
@return [String]
Taints to look for and trace in the JS data flow.
@return [TaintTracer]
{Proxy} for the `TaintTracer` JS interface.
@return [String]
Token used to namespace the injected JS code and avoid clashes.
Public Class Methods
# File lib/arachni/browser/javascript.rb, line 100 def self.events EVENTS end
@param [Browser] browser
# File lib/arachni/browser/javascript.rb, line 105 def initialize( browser ) @browser = browser @taint_tracer = TaintTracer.new( self ) @dom_monitor = DOMMonitor.new( self ) end
Public Instance Methods
@return (see TaintTracer#data_flow_sinks)
# File lib/arachni/browser/javascript.rb, line 230 def data_flow_sinks return [] if !supported? taint_tracer.data_flow_sinks[@taint] || [] end
@return [String]
JS code which will call the `TaintTracer.debug`, browser-side JS function.
# File lib/arachni/browser/javascript.rb, line 155 def debug_stub( *args ) taint_tracer.stub.function( :debug, *args ) end
@return (see TaintTracer#debug)
# File lib/arachni/browser/javascript.rb, line 218 def debugging_data return [] if !supported? taint_tracer.debugging_data end
@return [String]
Digest of the current DOM tree (i.e. node names and their attributes without text-nodes).
# File lib/arachni/browser/javascript.rb, line 256 def dom_digest return '' if !supported? dom_monitor.digest end
@return [String]
Digest of the available DOM events.
# File lib/arachni/browser/javascript.rb, line 263 def dom_event_digest return '' if !supported? dom_monitor.event_digest end
@note Will not include custom events.
@return [Array<Hash>]
Information about all DOM elements, including any registered event listeners.
# File lib/arachni/browser/javascript.rb, line 272 def each_dom_element_with_events( whitelist = [] ) return if !supported? start = 0 batch_size = EACH_DOM_ELEMENT_WITH_EVENTS_BATCH_SIZE loop do elements = dom_monitor.elements_with_events( start, batch_size, whitelist ) return if elements.empty? elements.each do |element| next if NO_EVENTS_FOR_ELEMENTS.include? element['tag_name'] events = {} element['events'].each do |event, handlers| events[event.to_sym] = handlers end element['events'] = events yield element end return if elements.size < batch_size start += elements.size end end
@return (see TaintTracer#execution_flow_sinks)
# File lib/arachni/browser/javascript.rb, line 224 def execution_flow_sinks return [] if !supported? taint_tracer.execution_flow_sinks end
@return (see TaintTracer#flush_data_flow_sinks)
# File lib/arachni/browser/javascript.rb, line 242 def flush_data_flow_sinks return [] if !supported? taint_tracer.flush_data_flow_sinks[@taint] || [] end
@return (see TaintTracer#flush_execution_flow_sinks)
# File lib/arachni/browser/javascript.rb, line 236 def flush_execution_flow_sinks return [] if !supported? taint_tracer.flush_execution_flow_sinks end
@param [HTTP::Response] response
Response whose {HTTP::Message#body} to check.
@return [Bool]
`true` if the {HTTP::Response response} {HTTP::Message#body} contains the code for the JS environment.
# File lib/arachni/browser/javascript.rb, line 129 def has_js_initializer?( response ) response.body.include? js_initialization_signal end
# File lib/arachni/browser/javascript.rb, line 212 def has_sinks? return false if !supported? taint_tracer.has_sinks( @taint ) end
# File lib/arachni/browser/javascript.rb, line 403 def html?( response ) # If the server says it's HTML dig deeper to ensure it. # We don't want wrong response headers messing up the JS env. response.html? && Parser.html?( response.body ) end
@note Will update the ‘Content-Length` header field.
@param [HTTP::Response] response
Installs our custom JS interfaces in the given `response`.
@see SCRIPT_BASE_URL
@see SCRIPT_LIBRARY
# File lib/arachni/browser/javascript.rb, line 336 def inject( response ) # Don't intercept our own stuff! return if response.url.start_with?( SCRIPT_BASE_URL ) # If it's a JS file, update our JS interfaces in case it has stuff that # can be tracked. # # This is necessary because new files can be required dynamically. if javascript?( response ) response.body.insert 0, <<-EOCODE #{js_comment} #{taint_tracer.stub.function( :update_tracers )}; #{dom_monitor.stub.function( :update_trackers )}; EOCODE response.body << ";\n" # Already has the JS initializer, so it's an HTML response; just update # taints and custom code. elsif has_js_initializer?( response ) update_taints( response.body, response ) update_custom_code( response.body ) elsif html?( response ) # Perform an update before each script. response.body.gsub!( /<script.*?>/i, "\\0\n #{js_comment} #{@taint_tracer.stub.function( :update_tracers )}; #{@dom_monitor.stub.function( :update_trackers )};\n\n" ) # Perform an update after each script. response.body.gsub!( /<\/script>/i, "\\0\n<script type=\"text/javascript\">" << "#{@taint_tracer.stub.function( :update_tracers )};" << "#{@dom_monitor.stub.function( :update_trackers )};" << "</script> #{html_comment}\n" ) # Include and initialize our JS interfaces. response.body.insert 0, <<-EOHTML <script src="#{script_url_for( :polyfills )}"></script> #{html_comment} <script src="#{script_url_for( :taint_tracer )}"></script> #{html_comment} <script src="#{script_url_for( :dom_monitor )}"></script> #{html_comment} <script> #{wrapped_dom_monitor_initializer} #{wrapped_taint_tracer_initializer( response )} #{js_initialization_signal}; #{wrapped_custom_code} </script> #{html_comment} EOHTML end true end
# File lib/arachni/browser/javascript.rb, line 399 def javascript?( response ) response.headers.content_type.to_s.downcase.include?( 'javascript' ) end
@return [String]
JS code which will call the `TaintTracer.log_data_flow_sink`, browser-side, JS function.
# File lib/arachni/browser/javascript.rb, line 149 def log_data_flow_sink_stub( *args ) taint_tracer.stub.function( :log_data_flow_sink, *args ) end
@return [String]
JS code which will call the `TaintTracer.log_execution_flow_sink`, browser-side, JS function.
# File lib/arachni/browser/javascript.rb, line 142 def log_execution_flow_sink_stub( *args ) taint_tracer.stub.function( :log_execution_flow_sink, *args ) end
@return [Bool]
`true` if our custom JS environment has been initialized.
# File lib/arachni/browser/javascript.rb, line 185 def ready? run( "return (typeof window._#{token} !== 'undefined' && document.readyState === 'complete')" ) rescue => e print_debug_exception e, 2 false end
@param [String] script
JS code to execute.
@return [Object]
Result of `script`.
# File lib/arachni/browser/javascript.rb, line 197 def run( *args ) @browser.selenium.execute_script *args end
Executes the given code but unwraps Watir elements.
@param [String] script
JS code to execute.
@return [Object]
Result of `script`.
# File lib/arachni/browser/javascript.rb, line 208 def run_without_elements( *args ) unwrap_elements run( *args ) end
@param [HTTP::Request] request
Request to process.
@param [HTTP::Response] response
Response to populate.
@return [Bool]
`true` if the request corresponded to a JS file and was served, `false` otherwise.
@see SCRIPT_BASE_URL
@see SCRIPT_LIBRARY
# File lib/arachni/browser/javascript.rb, line 318 def serve( request, response ) return false if !request.url.start_with?( SCRIPT_BASE_URL ) || !(script = read_script( request.parsed_url.path )) response.code = 200 response.body = script response.headers['content-type'] = 'text/javascript' response.headers['content-length'] = script.bytesize true end
Sets a custom ID attribute to elements with events but without a proper ID.
# File lib/arachni/browser/javascript.rb, line 248 def set_element_ids return '' if !supported? dom_monitor.setElementIds end
@return [Bool]
`true` if there is support for our JS environment in the current page, `false` otherwise.
@see has_js_initializer?
# File lib/arachni/browser/javascript.rb, line 116 def supported? # We won't have a response if the browser was steered towards an # out-of-scope resource. response = @browser.response response && has_js_initializer?( response ) end
@return [Array<Array>]
Arguments for JS `setTimeout` calls.
# File lib/arachni/browser/javascript.rb, line 302 def timeouts return [] if !supported? dom_monitor.timeouts end
Blocks until the browser page is {#ready? ready}.
# File lib/arachni/browser/javascript.rb, line 160 def wait_till_ready print_debug_level_2 'Waiting for custom JS...' if !supported? print_debug_level_2 '...unsupported.' return end t = Time.now while !ready? sleep 0.1 if Time.now - t > Options.browser_cluster.job_timeout print_debug_level_2 '...timed out.' return end end print_debug_level_2 '...done.' true end
Private Instance Methods
# File lib/arachni/browser/javascript.rb, line 502 def filesystem_path_for_script( filename ) @filesystem_path_for_script ||= {} @filesystem_path_for_script[filename] ||= begin name = "#{SCRIPT_LIBRARY}#{filename}" name << '.js' if !name.end_with?( '.js') File.expand_path( name ) end end
# File lib/arachni/browser/javascript.rb, line 415 def html_comment "<!-- Injected by #{self.class} -->" end
# File lib/arachni/browser/javascript.rb, line 411 def js_comment "// Injected by #{self.class}" end
# File lib/arachni/browser/javascript.rb, line 487 def js_initialization_signal "window._#{token} = true" end
# File lib/arachni/browser/javascript.rb, line 491 def read_script( filename ) @scripts ||= {} @scripts[filename] ||= SCRIPT_SOURCES[filesystem_path_for_script(filename)]. gsub( '_token', "_#{token}" ) end
# File lib/arachni/browser/javascript.rb, line 498 def script_exists?( filename ) SCRIPT_SOURCES.include? filesystem_path_for_script( filename ) end
# File lib/arachni/browser/javascript.rb, line 513 def script_url_for( filename ) if !script_exists?( filename ) fail ArgumentError, "Script #{filesystem_path_for_script( filename )} does not exist." end "#{SCRIPT_BASE_URL}#{filename}.js" end
# File lib/arachni/browser/javascript.rb, line 419 def taints( response ) taints = {} [@taint].flatten.compact.each do |t| taints[t] = { stop_at_first: false, trace: true } end # Include cookie names and values in the trace so that the browser will # be able to infer if they're being used, to avoid unnecessary audits. if Options.audit.cookie_doms? cookies = begin HTTP::Client.cookie_jar.for_url( response.url ) rescue print_debug "Could not get cookies for URL '#{response.url}' from Cookiejar (#{e})." print_debug_exception e HTTP::Client.cookies end cookies.each do |c| next if c.http_only? c.inputs.to_a.flatten.each do |input| next if input.empty? taints[input] ||= { stop_at_first: true, trace: false } end end end taints end
# File lib/arachni/browser/javascript.rb, line 542 def unwrap_element( element ) element.html rescue Selenium::WebDriver::Error::StaleElementReferenceError '' end
# File lib/arachni/browser/javascript.rb, line 522 def unwrap_elements( obj ) case obj when Watir::Element unwrap_element( obj ) when Selenium::WebDriver::Element unwrap_element( obj ) when Array obj.map { |e| unwrap_elements( e ) } when Hash obj.each { |k, v| obj[k] = unwrap_elements( v ) } obj else obj end end
# File lib/arachni/browser/javascript.rb, line 464 def update_custom_code( body ) body.gsub!( /\/\* #{token}_code_start \*\/(.*)\/\* #{token}_code_stop \*\//, wrapped_custom_code ) end
# File lib/arachni/browser/javascript.rb, line 457 def update_taints( body, response ) body.gsub!( /\/\* #{token}_initialize_start \*\/(.*)\/\* #{token}_initialize_stop \*\//, wrapped_taint_tracer_initializer( response ) ) end
# File lib/arachni/browser/javascript.rb, line 483 def wrapped_custom_code "/* #{token}_code_start */ #{custom_code} /* #{token}_code_stop */" end
# File lib/arachni/browser/javascript.rb, line 471 def wrapped_dom_monitor_initializer "/* #{token}_tokenDOMMonitor_initialize_start */ " << "#{@dom_monitor.stub.function( :initialize, Options.scope.dom_event_inheritance_limit )} " << "/* #{token}_tokenDOMMonitor_initialize_stop */" end
# File lib/arachni/browser/javascript.rb, line 477 def wrapped_taint_tracer_initializer( response ) "/* #{token}_initialize_start */ " << "#{@taint_tracer.stub.function( :initialize, taints( response ) )} " << "/* #{token}_initialize_stop */" end