Class: Arachni::Browser::Javascript

Inherits:
Object
  • Object
show all
Includes:
UI::Output, Utilities
Defined in:
lib/arachni/browser/javascript.rb,
lib/arachni/browser/javascript/proxy.rb,
lib/arachni/browser/javascript/dom_monitor.rb,
lib/arachni/browser/javascript/taint_tracer.rb,
lib/arachni/browser/javascript/taint_tracer/frame.rb,
lib/arachni/browser/javascript/taint_tracer/sink/base.rb,
lib/arachni/browser/javascript/taint_tracer/sink/data_flow.rb,
lib/arachni/browser/javascript/taint_tracer/sink/execution_flow.rb,
lib/arachni/browser/javascript/taint_tracer/frame/called_function.rb

Overview

Provides access to the Arachni::Browser‘s JavaScript environment, mainly helps group and organize functionality related to our custom Javascript interfaces.

Author:

Defined Under Namespace

Classes: DOMMonitor, Proxy, TaintTracer

Constant Summary collapse

CACHE =
{
    select_event_attributes: Support::Cache::LeastRecentlyPushed.new( 1_000 )
}
TOKEN =
'arachni_js_namespace'
SCRIPT_BASE_URL =

Returns URL to use when requesting our custom JS scripts.

Returns:

  • (String)

    URL to use when requesting our custom JS scripts.

'http://javascript.browser.arachni/'
SCRIPT_LIBRARY =

Returns Filesystem directory containing the JS scripts.

Returns:

  • (String)

    Filesystem directory containing the JS scripts.

"#{File.dirname( __FILE__ )}/javascript/scripts/"
SCRIPT_SOURCES =
Dir.glob("#{SCRIPT_LIBRARY}*.js").inject({}) do |h, path|
    h.merge!( path => IO.read(path) )
end
HTML_IDENTIFIERS =
['<!doctype html', '<html', '<head', '<body', '<title', '<script']
NO_EVENTS_FOR_ELEMENTS =
Set.new([
    :base, :bdo, :br, :head, :html, :iframe, :meta, :param, :script, :style,
    :title, :link
])
GLOBAL_EVENTS =

Events that apply to all elements.

[
    :onclick,
    :ondblclick,
    :onmousedown,
    :onmousemove,
    :onmouseout,
    :onmouseover,
    :onmouseup
]
EVENTS_PER_ELEMENT =

Special events for each element.

{
    body: [
              :onload
          ],

    form: [
              :onsubmit,
              :onreset
          ],

    # These need to be covered via Watir's API, #send_keys etc.
    input: [
              :onselect,
              :onchange,
              :onfocus,
              :onblur,
              :onkeydown,
              :onkeypress,
              :onkeyup,
              :oninput
          ],

    # These need to be covered via Watir's API, #send_keys etc.
    textarea: [
              :onselect,
              :onchange,
              :onfocus,
              :onblur,
              :onkeydown,
              :onkeypress,
              :onkeyup,
              :oninput
          ],

    select: [
              :onchange,
              :onfocus,
              :onblur
          ],

    button: [
              :onfocus,
              :onblur
          ],

    label: [
              :onfocus,
              :onblur
          ]
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utilities

#available_port, #bytes_to_kilobytes, #bytes_to_megabytes, #caller_name, #caller_path, #cookie_decode, #cookie_encode, #cookies_from_document, #cookies_from_file, #cookies_from_response, #exception_jail, #exclude_path?, #follow_protocol?, #form_decode, #form_encode, #forms_from_document, #forms_from_response, #full_and_absolute_url?, #generate_token, #get_path, #hms_to_seconds, #html_decode, #html_encode, #include_path?, #links_from_document, #links_from_response, #normalize_url, #page_from_response, #page_from_url, #parse_set_cookie, #path_in_domain?, #path_too_deep?, #port_available?, #rand_port, #random_seed, #redundant_path?, #regexp_array_match, #remove_constants, #request_parse_body, #seconds_to_hms, #skip_page?, #skip_path?, #skip_resource?, #skip_response?, #to_absolute, #uri_decode, #uri_encode, #uri_parse, #uri_parse_query, #uri_parser, #uri_rewrite

Methods included from UI::Output

#debug?, #debug_off, #debug_on, #disable_only_positives, #included, #mute, #muted?, #only_positives, #only_positives?, #print_bad, #print_debug, #print_debug_backtrace, #print_debug_level_1, #print_debug_level_2, #print_debug_level_3, #print_error, #print_error_backtrace, #print_exception, #print_info, #print_line, #print_ok, #print_status, #print_verbose, #reroute_to_file, #reroute_to_file?, reset_output_options, #unmute, #verbose?, #verbose_on

Constructor Details

#initialize(browser) ⇒ Javascript

Returns a new instance of Javascript.

Parameters:



163
164
165
166
167
# File 'lib/arachni/browser/javascript.rb', line 163

def initialize( browser )
    @browser      = browser
    @taint_tracer = TaintTracer.new( self )
    @dom_monitor  = DOMMonitor.new( self )
end

Instance Attribute Details

#custom_codeString

Returns Inject custom JS code right after the initialization of the custom JS interfaces.

Returns:

  • (String)

    Inject custom JS code right after the initialization of the custom JS interfaces.



123
124
125
# File 'lib/arachni/browser/javascript.rb', line 123

def custom_code
  @custom_code
end

#dom_monitorDOMMonitor (readonly)

Returns Proxy for the ‘DOMMonitor` JS interface.

Returns:



127
128
129
# File 'lib/arachni/browser/javascript.rb', line 127

def dom_monitor
  @dom_monitor
end

#taintString

Returns Taints to look for and trace in the JS data flow.

Returns:

  • (String)

    Taints to look for and trace in the JS data flow.



118
119
120
# File 'lib/arachni/browser/javascript.rb', line 118

def taint
  @taint
end

#taint_tracerTaintTracer (readonly)

Returns Proxy for the ‘TaintTracer` JS interface.

Returns:



131
132
133
# File 'lib/arachni/browser/javascript.rb', line 131

def taint_tracer
  @taint_tracer
end

#tokenString

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

Returns:

  • (String)

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



114
115
116
# File 'lib/arachni/browser/javascript.rb', line 114

def token
  @token
end

Class Method Details

.event_whitelistObject



137
138
139
# File 'lib/arachni/browser/javascript.rb', line 137

def self.event_whitelist
    @event_whitelist ||= Set.new( events.flatten.map(&:to_s) )
end

.eventsObject



133
134
135
# File 'lib/arachni/browser/javascript.rb', line 133

def self.events
    GLOBAL_EVENTS | EVENTS_PER_ELEMENT.values.flatten.uniq
end

.events_for(element) ⇒ Array<Symbol>

Returns Events for ‘element`.

Parameters:

  • element (Symbol)

Returns:

  • (Array<Symbol>)

    Events for ‘element`.



145
146
147
# File 'lib/arachni/browser/javascript.rb', line 145

def self.events_for( element )
    GLOBAL_EVENTS | EVENTS_PER_ELEMENT[element.to_sym]
end

.select_event_attributes(attributes = {}) ⇒ Hash

Returns ‘attributes` that include events.

Parameters:

  • attributes (Hash) (defaults to: {})

    Element attributes.

Returns:



154
155
156
157
158
159
160
# File 'lib/arachni/browser/javascript.rb', line 154

def self.select_event_attributes( attributes = {} )
    CACHE[:select_event_attributes][attributes] ||=
        attributes.inject({}) do |h, (event, handler)|
            next h if !event_whitelist.include?( event.to_s )
            h.merge!( event.to_sym => handler )
        end
end

Instance Method Details

#data_flow_sinksArray<Sink::DataFlow>

Returns JS data flow sink data.

Returns:

  • (Array<Sink::DataFlow>)

    JS data flow sink data.



262
263
264
265
# File 'lib/arachni/browser/javascript.rb', line 262

def data_flow_sinks
    return [] if !supported?
    taint_tracer.data_flow_sinks[@taint] || []
end

#debug_stub(*args) ⇒ String

Returns JS code which will call the ‘TaintTracer.debug`, browser-side JS function.

Returns:

  • (String)

    JS code which will call the ‘TaintTracer.debug`, browser-side JS function.



213
214
215
# File 'lib/arachni/browser/javascript.rb', line 213

def debug_stub( *args )
    taint_tracer.stub.function( :debug, *args )
end

#debugging_dataObject



250
251
252
253
# File 'lib/arachni/browser/javascript.rb', line 250

def debugging_data
    return [] if !supported?
    taint_tracer.debugging_data
end

#dom_digestString

Returns Digest of the current DOM tree (i.e. node names and their attributes without text-nodes).

Returns:

  • (String)

    Digest of the current DOM tree (i.e. node names and their attributes without text-nodes).



288
289
290
291
# File 'lib/arachni/browser/javascript.rb', line 288

def dom_digest
    return '' if !supported?
    dom_monitor.digest
end

#dom_elements_with_eventsArray<Hash>

Note:

Will not include custom events.

Returns Information about all DOM elements, including any registered event listeners.

Returns:

  • (Array<Hash>)

    Information about all DOM elements, including any registered event listeners.



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/arachni/browser/javascript.rb', line 297

def dom_elements_with_events
    return [] if !supported?

    dom_monitor.elements_with_events.map do |element|
        next if NO_EVENTS_FOR_ELEMENTS.include? element['tag_name'].to_sym

        attributes = element['attributes']

        element['events'] = (element['events'].map do |event, fn|
            next if !(self.class.event_whitelist.include?( event ) ||
                self.class.event_whitelist.include?( "on#{event}" ))

            [event.to_sym, fn]
        end.compact)

        element['events'] |= self.class.select_event_attributes( attributes ).to_a

        element
    end.compact
end

#execution_flow_sinksArray<Sink::ExecutionFlow>

Returns JS execution flow sink data.

Returns:

  • (Array<Sink::ExecutionFlow>)

    JS execution flow sink data.



256
257
258
259
# File 'lib/arachni/browser/javascript.rb', line 256

def execution_flow_sinks
    return [] if !supported?
    taint_tracer.execution_flow_sinks
end

#flush_data_flow_sinksArray<Sink::DataFlow>

Returns and clears #data_flow_sinks.

Returns:



274
275
276
277
# File 'lib/arachni/browser/javascript.rb', line 274

def flush_data_flow_sinks
    return [] if !supported?
    taint_tracer.flush_data_flow_sinks[@taint] || []
end

#flush_execution_flow_sinksArray<Sink::ExecutionFlow>

Returns and clears #execution_flow_sinks.

Returns:



268
269
270
271
# File 'lib/arachni/browser/javascript.rb', line 268

def flush_execution_flow_sinks
    return [] if !supported?
    taint_tracer.flush_execution_flow_sinks
end

#has_js_initializer?(response) ⇒ Bool

Returns ‘true` if the response HTTP::Message#body contains the code for the JS environment.

Parameters:

Returns:



187
188
189
# File 'lib/arachni/browser/javascript.rb', line 187

def has_js_initializer?( response )
    response.body.include? js_initialization_signal
end

#html?(response) ⇒ Boolean

Returns:

  • (Boolean)


435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/arachni/browser/javascript.rb', line 435

def html?( response )
    return false if response.body.empty?

    # We only care about HTML.
    return false if !response.headers.content_type.to_s.downcase.start_with?( 'text/html' )

    # Let's check that the response at least looks like it contains HTML
    # code of interest.
    body = response.body.downcase
    return false if !HTML_IDENTIFIERS.find { |tag| body.include? tag.downcase }

    # The last check isn't fool-proof, so don't do it when loading the page
    # for the first time, but only when the page loads stuff via AJAX and whatnot.
    #
    # Well, we can be pretty sure that the root page will be HTML anyways.
    return true if @browser.last_url == response.url

    # Finally, verify that we're really working with markup (hopefully HTML)
    # and that the previous checks weren't just flukes matching some other
    # kind of document.
    #
    # For example, it may have been JSON with the wrong content-type that
    # includes HTML -- it happens.
    begin
        return false if Nokogiri::XML( response.body ).children.empty?
    rescue => e
        print_debug "Does not look like HTML: #{response.url}"
        print_debug "\n#{response.body}"
        print_debug_exception e
        return false
    end

    true
end

#inject(response) ⇒ Object

Note:

Will update the ‘Content-Length` header field.

Parameters:

  • response (HTTP::Response)

    Installs our custom JS interfaces in the given ‘response`.

See Also:



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/arachni/browser/javascript.rb', line 361

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 = <<-EOCODE
            #{js_comment}
            #{taint_tracer.stub.function( :update_tracers )};
            #{dom_monitor.stub.function( :update_trackers )};

            #{response.body};
        EOCODE

    # Already has the JS initializer, so it's an HTML response; just update
    # taints and custom code.
    elsif has_js_initializer?( response )

        body = response.body.dup

        update_taints( body )
        update_custom_code( body )

        response.body = body

    elsif html?( response )
        body = response.body.dup

        # Perform an update before each script.
        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.
        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 = <<-EOHTML
<script src="#{script_url_for( :taint_tracer )}"></script> #{html_comment}
<script src="#{script_url_for( :dom_monitor )}"></script> #{html_comment}
<script>
#{wrapped_taint_tracer_initializer}
#{js_initialization_signal};

#{wrapped_custom_code}
</script> #{html_comment}

#{body}
        EOHTML
    end

    response.headers['content-length'] = response.body.size

    true
end

#intervalsArray<Array>

Returns Arguments for JS ‘setInterval` calls.

Returns:

  • (Array<Array>)

    Arguments for JS ‘setInterval` calls.



327
328
329
330
# File 'lib/arachni/browser/javascript.rb', line 327

def intervals
    return [] if !supported?
    dom_monitor.intervals
end

#javascript?(response) ⇒ Boolean

Returns:

  • (Boolean)


431
432
433
# File 'lib/arachni/browser/javascript.rb', line 431

def javascript?( response )
    response.headers.content_type.to_s.downcase.include?( 'javascript' )
end

#log_data_flow_sink_stub(*args) ⇒ String

Returns JS code which will call the ‘TaintTracer.log_data_flow_sink`, browser-side, JS function.

Returns:

  • (String)

    JS code which will call the ‘TaintTracer.log_data_flow_sink`, browser-side, JS function.



207
208
209
# File 'lib/arachni/browser/javascript.rb', line 207

def log_data_flow_sink_stub( *args )
    taint_tracer.stub.function( :log_data_flow_sink, *args )
end

#log_execution_flow_sink_stub(*args) ⇒ String

Returns JS code which will call the ‘TaintTracer.log_execution_flow_sink`, browser-side, JS function.

Returns:

  • (String)

    JS code which will call the ‘TaintTracer.log_execution_flow_sink`, browser-side, JS function.



200
201
202
# File 'lib/arachni/browser/javascript.rb', line 200

def log_execution_flow_sink_stub( *args )
    taint_tracer.stub.function( :log_execution_flow_sink, *args )
end

#ready?Bool

Returns ‘true` if our custom JS environment has been initialized.

Returns:

  • (Bool)

    ‘true` if our custom JS environment has been initialized.



225
226
227
# File 'lib/arachni/browser/javascript.rb', line 225

def ready?
    !!run( "return window._#{token}" ) rescue false
end

#run(script) ⇒ Object

Returns Result of ‘script`.

Parameters:

  • script (String)

    JS code to execute.

Returns:

  • (Object)

    Result of ‘script`.



234
235
236
# File 'lib/arachni/browser/javascript.rb', line 234

def run( script )
    @browser.watir.execute_script script
end

#run_without_elements(script) ⇒ Object

Executes the given code but unwraps Watir elements.

Parameters:

  • script (String)

    JS code to execute.

Returns:

  • (Object)

    Result of ‘script`.



245
246
247
# File 'lib/arachni/browser/javascript.rb', line 245

def run_without_elements( script )
    unwrap_elements run( script )
end

#serve(request, response) ⇒ Bool

Returns ‘true` if the request corresponded to a JS file and was served, `false` otherwise.

Parameters:

Returns:

  • (Bool)

    ‘true` if the request corresponded to a JS file and was served, `false` otherwise.

See Also:



343
344
345
346
347
348
349
350
351
352
# File 'lib/arachni/browser/javascript.rb', line 343

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_idsObject

Sets a custom ID attribute to elements with events but without a proper ID.



280
281
282
283
# File 'lib/arachni/browser/javascript.rb', line 280

def set_element_ids
    return '' if !supported?
    dom_monitor.setElementIds
end

#supported?Bool

Returns ‘true` if there is support for our JS environment in the current page, `false` otherwise.

Returns:

  • (Bool)

    ‘true` if there is support for our JS environment in the current page, `false` otherwise.

See Also:



174
175
176
177
178
179
# File 'lib/arachni/browser/javascript.rb', line 174

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

#timeoutsArray<Array>

Returns Arguments for JS ‘setTimeout` calls.

Returns:

  • (Array<Array>)

    Arguments for JS ‘setTimeout` calls.



320
321
322
323
# File 'lib/arachni/browser/javascript.rb', line 320

def timeouts
    return [] if !supported?
    dom_monitor.timeouts
end

#wait_till_readyObject

Blocks until the browser page is ready.



218
219
220
221
# File 'lib/arachni/browser/javascript.rb', line 218

def wait_till_ready
    return if !supported?
    sleep 0.1 while !ready?
end