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

custom_code[RW]

@return [String]

Inject custom JS code right after the initialization of the custom
JS interfaces.
dom_monitor[R]

@return [DOMMonitor]

{Proxy} for the `DOMMonitor` JS interface.
taint[RW]

@return [String]

Taints to look for and trace in the JS data flow.
taint_tracer[R]

@return [TaintTracer]

{Proxy} for the `TaintTracer` JS interface.
token[RW]

@return [String]

Token used to namespace the injected JS code and avoid clashes.

Public Class Methods

events() click to toggle source
# File lib/arachni/browser/javascript.rb, line 100
def self.events
    EVENTS
end
new( browser ) click to toggle source

@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

data_flow_sinks() click to toggle source

@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
debug_stub( *args ) click to toggle source

@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
debugging_data() click to toggle source

@return (see TaintTracer#debug)

# File lib/arachni/browser/javascript.rb, line 218
def debugging_data
    return [] if !supported?
    taint_tracer.debugging_data
end
dom_digest() click to toggle source

@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
dom_event_digest() click to toggle source

@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
each_dom_element_with_events( whitelist = [] ) { |element| ... } click to toggle source

@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
execution_flow_sinks() click to toggle source

@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
flush_data_flow_sinks() click to toggle source

@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
flush_execution_flow_sinks() click to toggle source

@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
has_js_initializer?( response ) click to toggle source

@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
has_sinks?() click to toggle source
# File lib/arachni/browser/javascript.rb, line 212
def has_sinks?
    return false if !supported?
    taint_tracer.has_sinks( @taint )
end
html?( response ) click to toggle source
# 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
inject( response ) click to toggle source

@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
javascript?( response ) click to toggle source
# File lib/arachni/browser/javascript.rb, line 399
def javascript?( response )
    response.headers.content_type.to_s.downcase.include?( 'javascript' )
end
log_data_flow_sink_stub( *args ) click to toggle source

@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
log_execution_flow_sink_stub( *args ) click to toggle source

@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
ready?() click to toggle source

@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
run( *args ) click to toggle source

@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
run_without_elements( *args ) click to toggle source

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
serve( request, response ) click to toggle source

@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
set_element_ids() click to toggle source

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
supported?() click to toggle source

@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
timeouts() click to toggle source

@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
wait_till_ready() click to toggle source

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

filesystem_path_for_script( filename ) click to toggle source
# 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
html_comment() click to toggle source
# File lib/arachni/browser/javascript.rb, line 415
def html_comment
    "<!-- Injected by #{self.class} -->"
end
js_comment() click to toggle source
# File lib/arachni/browser/javascript.rb, line 411
def js_comment
    "// Injected by #{self.class}"
end
js_initialization_signal() click to toggle source
# File lib/arachni/browser/javascript.rb, line 487
def js_initialization_signal
    "window._#{token} = true"
end
read_script( filename ) click to toggle source
# 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
script_exists?( filename ) click to toggle source
# File lib/arachni/browser/javascript.rb, line 498
def script_exists?( filename )
    SCRIPT_SOURCES.include? filesystem_path_for_script( filename )
end
script_url_for( filename ) click to toggle source
# 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
taints( response ) click to toggle source
# 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
unwrap_element( element ) click to toggle source
# File lib/arachni/browser/javascript.rb, line 542
def unwrap_element( element )
    element.html
rescue Selenium::WebDriver::Error::StaleElementReferenceError
    ''
end
unwrap_elements( obj ) click to toggle source
# 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
update_custom_code( body ) click to toggle source
# 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
update_taints( body, response ) click to toggle source
# 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
wrapped_custom_code() click to toggle source
# File lib/arachni/browser/javascript.rb, line 483
def wrapped_custom_code
    "/* #{token}_code_start */ #{custom_code} /* #{token}_code_stop */"
end
wrapped_dom_monitor_initializer() click to toggle source
# 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
wrapped_taint_tracer_initializer( response ) click to toggle source
# 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