Class: Arachni::Browser

Inherits:
Object show all
Includes:
Support::Mixins::Observable, UI::Output, Utilities
Defined in:
lib/arachni/browser.rb,
lib/arachni/browser/javascript.rb,
lib/arachni/browser/element_locator.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

Note:

Depends on PhantomJS 1.9.2.

Real browser driver providing DOM/JS/AJAX support.

Direct Known Subclasses

Arachni::BrowserCluster::Worker

Defined Under Namespace

Classes: ElementLocator, Error, Javascript

Constant Summary collapse

PHANTOMJS_SPAWN_TIMEOUT =

How much time to wait for the PhantomJS process to spawn before respawning.

4
ELEMENT_APPEARANCE_TIMEOUT =

How much time to wait for a targeted HTML element to appear on the page after the page is loaded.

5
WATIR_COM_TIMEOUT =

Let the browser take as long as it needs to complete an operation.

3600
ASSET_EXTENSIONS =

1 hour.

Set.new(
    ['css', 'js', 'jpg', 'jpeg', 'png', 'gif', 'woff', 'json']
)
ASSET_EXTRACTORS =
[
    /<\s*link.*?href=['"](.*?)['"].*?>/im,
    /<\s*script.*?src=['"](.*?)['"].*?>/im,
    /<\s*img.*?src=['"](.*?)['"].*?>/im,
    /<\s*input.*?src=['"](.*?)['"].*?>/im,
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Support::Mixins::Observable

included

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_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(options = {}) ⇒ Browser

Returns a new instance of Browser.

Parameters:

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

Options Hash (options):

  • :concurrency (Integer)

    Maximum number of concurrent connections.

  • :store_pages (Bool) — default: true

    Whether to store pages in addition to just passing them to #on_new_page.

  • :width (Integer) — default: 1600

    Window width.

  • :height (Integer) — default: 1200

    Window height.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/arachni/browser.rb', line 157

def initialize( options = {} )
    super()
    @options = options.dup

    @ignore_scope = options[:ignore_scope]

    @width  = options[:width]  || 1600
    @height = options[:height] || 1200

    @proxy = HTTP::ProxyServer.new(
        concurrency:      @options[:concurrency],
        address:          '127.0.0.1',
        request_handler:  proc do |request, response|
            synchronize { exception_jail { request_handler( request, response ) } }
        end,
        response_handler: proc do |request, response|
            synchronize { exception_jail { response_handler( request, response ) } }
        end
    )

    @options[:store_pages] = true if !@options.include?( :store_pages )

    @proxy.start_async

    @watir = ::Watir::Browser.new( selenium )

    # User-controlled response cache, by URL.
    @cache = Support::Cache::LeastRecentlyUsed.new( 200 )

    # User-controlled preloaded responses, by URL.
    @preloads = {}

    # Captured pages -- populated by #capture.
    @captured_pages = []

    # Snapshots of the working page resulting from firing of events and
    # clicking of JS links.
    @page_snapshots = {}

    # Same as @page_snapshots but it doesn't deduplicate and only contains
    # pages with sink (Page::DOM#sink) data as populated by Javascript#flush_sink.
    @page_snapshots_with_sinks = []

    # Captures HTTP::Response objects per URL for open windows.
    @window_responses = {}

    # Keeps track of resources which should be skipped -- like already fired
    # events and clicked links etc.
    @skip_states = Support::LookUp::HashSet.new( hasher: :persistent_hash )

    @transitions = []
    @request_transitions = []
    @add_request_transitions = true

    # Last loaded URL.
    @last_url = nil

    @javascript = Javascript.new( self )

    ensure_open_window
end

Instance Attribute Details

#javascriptJavascript (readonly)

Returns:



107
108
109
# File 'lib/arachni/browser.rb', line 107

def javascript
  @javascript
end

#last_dom_urlObject (readonly)

Returns the value of attribute last_dom_url.



121
122
123
# File 'lib/arachni/browser.rb', line 121

def last_dom_url
  @last_dom_url
end

#last_urlObject (readonly)

Returns the value of attribute last_url.



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

def last_url
  @last_url
end

#page_snapshots_with_sinksArray<Page> (readonly)

Returns Same as #page_snapshots but it doesn’t deduplicate and only contains pages with sink (Page::DOM#data_flow_sinks or Page::DOM#execution_flow_sinks) data as populated by Arachni::Browser::Javascript#data_flow_sinks and Arachni::Browser::Javascript#execution_flow_sinks.



104
105
106
# File 'lib/arachni/browser.rb', line 104

def page_snapshots_with_sinks
  @page_snapshots_with_sinks
end

#pidInteger (readonly)

Returns:

  • (Integer)


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

def pid
  @pid
end

#preloadsHash (readonly)

Returns Preloaded resources, by URL.

Returns:

  • (Hash)

    Preloaded resources, by URL.



89
90
91
# File 'lib/arachni/browser.rb', line 89

def preloads
  @preloads
end

#skip_statesSupport::LookUp::HashSet (readonly)

Returns States that have been visited and should be skipped.

Returns:

See Also:

  • #skip_state
  • #skip_state?


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

def skip_states
  @skip_states
end

#transitionsArray<Page::DOM::Transition> (readonly)



85
86
87
# File 'lib/arachni/browser.rb', line 85

def transitions
  @transitions
end

#watirWatir::Browser (readonly)

Returns Watir driver interface.

Returns:

  • (Watir::Browser)

    Watir driver interface.



93
94
95
# File 'lib/arachni/browser.rb', line 93

def watir
  @watir
end

Class Method Details

.add_asset_domain(url) ⇒ Object



140
141
142
143
144
145
146
# File 'lib/arachni/browser.rb', line 140

def self.add_asset_domain( url )
    return if url.to_s.empty?
    return if !(curl = Arachni::URI( url ))
    return if !(domain = curl.domain)

    asset_domains << domain
end

.asset_domainsObject



135
136
137
# File 'lib/arachni/browser.rb', line 135

def self.asset_domains
    @asset_domains ||= Set.new
end

.executableString

Returns Path to the PhantomJS executable.

Returns:

  • (String)

    Path to the PhantomJS executable.



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

def self.executable
    Selenium::WebDriver::PhantomJS.path
end

.has_executable?Bool

Returns ‘true` if a supported browser is in the OS PATH, `false` otherwise.

Returns:

  • (Bool)

    ‘true` if a supported browser is in the OS PATH, `false` otherwise.



125
126
127
# File 'lib/arachni/browser.rb', line 125

def self.has_executable?
    !!executable
end

Instance Method Details

#cache(resource = nil) ⇒ Object

Parameters:

  • resource (HTTP::Response, Page) (defaults to: nil)

    Cache a resource in order to be instantly available by URL via #load.



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

def cache( resource = nil )
    return @cache if !resource

    response =  case resource
                    when HTTP::Response
                        resource

                    when Page
                        resource.response

                    else
                        fail Error::Load,
                             "Can't load resource of type #{resource.class}."
                end

    save_response response
    @cache[response.url] = response
    response.url
end

#capture?Bool

Returns ‘true` if request capturing is enabled, `false` otherwise.

Returns:

  • (Bool)

    ‘true` if request capturing is enabled, `false` otherwise.

See Also:



769
770
771
# File 'lib/arachni/browser.rb', line 769

def capture?
    !!@capture
end

#capture_snapshot(transition = nil) ⇒ Object



849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
# File 'lib/arachni/browser.rb', line 849

def capture_snapshot( transition = nil )
    pages = []

    request_transitions = flush_request_transitions
    transitions = ([transition] + request_transitions).flatten.compact

    begin
        # Skip about:blank windows.
        watir.windows( url: /^http/ ).each do |window|
            window.use do
                next if !(page = to_page)

                if pages.empty?
                    transitions.each do |t|
                        @transitions << t
                        page.dom.push_transition t
                    end
                end

                capture_snapshot_with_sink( page )

                unique_id  = self.snapshot_id
                next if skip_state? unique_id
                skip_state unique_id

                notify_on_new_page( page )

                if store_pages?
                    @page_snapshots[unique_id.hash] = page
                    pages << page
                end
            end
        end
    rescue => e
        print_debug "Could not capture snapshot for: #{@last_url}"

        if transition
            print_debug "-- #{transition}"
        end

        print_debug
        print_debug_exception e
    end

    pages
end

#captured_pagesArray<Page>

Returns Captured HTTP requests performed by the web page (AJAX etc.) converted into forms of pages to assist with analysis and audit.

Returns:

  • (Array<Page>)

    Captured HTTP requests performed by the web page (AJAX etc.) converted into forms of pages to assist with analysis and audit.



783
784
785
# File 'lib/arachni/browser.rb', line 783

def captured_pages
    @captured_pages
end

#clear_buffersObject



219
220
221
222
223
224
225
226
227
228
# File 'lib/arachni/browser.rb', line 219

def clear_buffers
    synchronize do
        @preloads.clear
        @cache.clear
        @captured_pages.clear
        @page_snapshots.clear
        @page_snapshots_with_sinks.clear
        @window_responses.clear
    end
end

#cookiesArray<Cookie>

Returns Browser cookies.

Returns:



922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
# File 'lib/arachni/browser.rb', line 922

def cookies
    js_cookies = begin
        # Watir doesn't tell us if cookies are HttpOnly, so we need to figure
        # this out ourselves, by checking for JS visibility.
        javascript.run( 'return document.cookie' )
    # We may not have a page.
    rescue Selenium::WebDriver::Error::WebDriverError
        ''
    end

    watir.cookies.to_a.map do |c|
        original_name = c[:name].to_s

        c[:path]     = '/' if c[:path] == '//'
        c[:name]     = Cookie.decode( c[:name].to_s )
        c[:value]    = Cookie.value_to_v0( c[:value].to_s )
        c[:httponly] = !js_cookies.include?( original_name )

        Cookie.new c.merge( url: @last_url || url )
    end
end

#distribute_event(page, locator, event) ⇒ Object

Note:

Only used when running as part of Arachni::BrowserCluster to distribute page analysis across a pool of browsers.

Distributes the triggering of ‘event` on the element at `element_index` on `page`.

Parameters:



587
588
589
# File 'lib/arachni/browser.rb', line 587

def distribute_event( page, locator, event )
    trigger_event( page, locator, event )
end

#each_element_with_events(mark_state = true) {|ElementLocator, Array<Symbol>| ... } ⇒ Object

Note:

Will skip non-visible elements as they can’t be manipulated.

Iterates over all elements which have events and passes their info to the given block.

Parameters:

  • mark_state (Bool) (defaults to: true)

    Mark each element/events as visited and skip it if it has already been seen.

Yields:

  • (ElementLocator, Array<Symbol>)

    Hash with information about the element, its tag name, applicable events along with their handlers and attributes.



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
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/arachni/browser.rb', line 444

def each_element_with_events( mark_state = true )
    current_url = url

    javascript.dom_elements_with_events.each do |element|
        tag_name   = element['tag_name']
        attributes = element['attributes']
        events     = element['events']

        case tag_name
            when 'a'
                href = attributes['href'].to_s

                if !href.empty?
                    if href.downcase.start_with?( 'javascript:' )
                        events << [ :click, href ]
                    else
                        next if skip_path?( to_absolute( href, current_url ) )
                    end
                end

            when 'input'
                if attributes['type'].to_s.downcase == 'image'
                    events << [ :click, 'image' ]
                end

            when 'form'
                action = attributes['action'].to_s

                if !action.empty?
                    if action.downcase.start_with?( 'javascript:' )
                        events << [ :submit, action ]
                    else
                        next if skip_path?( to_absolute( action, current_url ) )
                    end
                end
        end

        next if events.empty?

        if mark_state
            state = "#{tag_name}#{attributes}#{events}"
            next if skip_state?( state )
            skip_state state
        end

        yield ElementLocator.new( tag_name: tag_name, attributes: attributes ),
                events
    end

    self
end

#explore_and_flush(depth = nil) ⇒ Array<Page>

Explores the browser’s DOM tree and captures page snapshots for each state change until there are no more available.

Parameters:

  • depth (Integer) (defaults to: nil)

    How deep to go into the DOM tree.

Returns:

  • (Array<Page>)

    Page snapshots for each state.



416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/arachni/browser.rb', line 416

def explore_and_flush( depth = nil )
    pages         = [ to_page ]
    current_depth = 0

    loop do
        bcnt   = pages.size
        pages |= pages.map { |p| load( p ).trigger_events.flush_pages }.flatten

        break if pages.size == bcnt || (depth && depth >= current_depth)

        current_depth += 1
    end

    pages.compact
end

#fire_event(element, event, options = {}) ⇒ Page::DOM::Transition, false

Triggers ‘event` on `element`.

Parameters:

Options Hash (options):

Returns:



630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
# File 'lib/arachni/browser.rb', line 630

def fire_event( element, event, options = {} )
    event   = event.to_s.downcase.sub( /^on/, '' ).to_sym
    locator = nil

    options[:inputs] = options[:inputs].my_stringify if options[:inputs]

    if element.is_a? ElementLocator
        locator = element

        begin
            element = element.locate( self )
        rescue Selenium::WebDriver::Error::WebDriverError,
            Watir::Exception::Error => e

            print_debug "Element '#{element.inspect}' could not be " <<
                            "located for triggering '#{event}'."
            print_debug
            print_debug_exception e
            return
        end
    end

    # The page may need a bit to settle and the element is lazily located
    # by Watir so give it a few tries.
    begin
        with_timeout ELEMENT_APPEARANCE_TIMEOUT do
            sleep 0.1 while !element.exists?
        end
    rescue Timeout::Error
        print_debug_level_2 "#{element.inspect} did not appear in " <<
                                "#{ELEMENT_APPEARANCE_TIMEOUT}."
        return
    end

    if !element.visible?
        print_debug_level_2 "#{element.inspect} is not visible, skipping..."
        return
    end

    if locator
        opening_tag = locator.to_s
        tag_name    = locator.tag_name
    else
        opening_tag = element.opening_tag
        tag_name    = element.tag_name
        locator     = ElementLocator.from_html( opening_tag )
    end

    print_debug_level_2 "#{__method__} [start]: #{event} (#{options}) #{locator}"

    tag_name = tag_name.to_sym

    notify_on_fire_event( element, event )

    tries = 0
    begin
        Page::DOM::Transition.new( locator, event, options ) do
            had_special_trigger = false

            if tag_name == :form
                fill_in_form_inputs( element, options[:inputs] )

                if event == :submit
                    element.to_subtype.submit
                    had_special_trigger = true
                end
            elsif tag_name == :input && event == :click &&
                    element.attribute_value(:type) == 'image'

                element.to_subtype.click
                had_special_trigger = true

            elsif [:keyup, :keypress, :keydown, :change, :input, :focus, :blur, :select].include? event

                # Some of these need an explicit event triggers.
                had_special_trigger = true if ![:change, :blur, :focus, :select].include? event

                element.send_keys( (options[:value] || value_for( element )).to_s )
            end

            element.fire_event( event ) if !had_special_trigger

            print_debug_level_2 "#{__method__} [waiting for requests]: #{event} (#{options}) #{locator}"
            wait_for_pending_requests
            print_debug_level_2 "#{__method__} [done waiting for requests]: #{event} (#{options}) #{locator}"

            # puts source_with_line_numbers
            print_debug_level_2 "#{__method__} [done]: #{event} (#{options}) #{locator}"
        end
    rescue Selenium::WebDriver::Error::WebDriverError,
        Watir::Exception::Error => e

        sleep 0.1

        tries += 1
        retry if tries < 5

        print_debug "Error when triggering event for: #{url}"
        print_debug "-- '#{event}' on: #{opening_tag}"
        print_debug
        print_debug_exception e

        nil
    end
end

#flush_page_snapshots_with_sinksArray<Page>

Returns #page_snapshots_with_sinks and flushes it.

Returns:



898
899
900
901
902
# File 'lib/arachni/browser.rb', line 898

def flush_page_snapshots_with_sinks
    @page_snapshots_with_sinks.dup
ensure
    @page_snapshots_with_sinks.clear
end

#flush_pagesArray<Page>

Returns Flushes and returns the captured and snapshot pages.

Returns:

See Also:



913
914
915
916
917
918
# File 'lib/arachni/browser.rb', line 913

def flush_pages
    captured_pages + page_snapshots
ensure
    @captured_pages.clear
    @page_snapshots.clear
end

#goto(url, options = {}) ⇒ Page::DOM::Transition

Returns Transition used to replay the resource visit.

Parameters:

  • url (String)

    Loads the given URL in the browser.

  • options (Hash) (defaults to: {})
  • [Bool] (Hash)

    a customizable set of options

  • [Array<Cookie>] (Hash)

    a customizable set of options

Returns:



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
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
# File 'lib/arachni/browser.rb', line 330

def goto( url, options = {} )
    take_snapshot      = options.include?(:take_snapshot) ?
        options[:take_snapshot] : true
    extra_cookies      = options[:cookies] || {}
    update_transitions = options.include?(:update_transitions) ?
        options[:update_transitions] : true

    pre_add_request_transitions = @add_request_transitions
    if !update_transitions
        @add_request_transitions = false
    end

    @last_url = Arachni::URI( url ).to_s
    self.class.add_asset_domain @last_url

    ensure_open_window

    load_cookies url, extra_cookies

    transition = Page::DOM::Transition.new( :page, :load,
        url:     url,
        cookies: extra_cookies
    ) do
        watir.goto url

        @javascript.wait_till_ready
        wait_for_pending_requests
        wait_for_timers

        url = watir.url
        Options.browser_cluster.css_to_wait_for( url ).each do |css|
            print_info "Waiting for #{css.inspect} to appear for: #{url}"

            begin
                watir.element( css: css ).
                    wait_until_present( Options.browser_cluster.job_timeout )

                print_info "#{css.inspect} appeared for: #{url}"
            rescue Watir::Wait::TimeoutError
                print_bad "#{css.inspect} did not appeared for: #{url}"
            end

        end

        javascript.set_element_ids
    end

    if @add_request_transitions
        @transitions << transition
    end

    @add_request_transitions = pre_add_request_transitions

    HTTP::Client.update_cookies cookies

    # Capture the page at its initial state.
    capture_snapshot if take_snapshot

    transition
end

#inspectObject



1007
1008
1009
1010
1011
1012
1013
# File 'lib/arachni/browser.rb', line 1007

def inspect
    s = "#<#{self.class} "
    s << "pid=#{@pid} "
    s << "last-url=#{@last_url.inspect} "
    s << "transitions=#{@transitions.size}"
    s << '>'
end

#load(resource, options = {}) ⇒ Browser

Returns ‘self`.

Parameters:

  • resource (String, HTTP::Response, Page)

    Loads the given resource in the browser. If it is a string it will be treated like a URL.

Returns:



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/arachni/browser.rb', line 244

def load( resource, options = {} )
    @last_dom_url = nil

    case resource
        when String
            goto resource, options

        when HTTP::Response
            goto preload( resource ), options

        when Page
            HTTP::Client.update_cookies resource.cookie_jar

            @transitions = resource.dom.transitions.dup
            update_skip_states resource.dom.skip_states

            @last_dom_url = resource.dom.url

            @add_request_transitions = false if @transitions.any?
            resource.dom.restore self
            @add_request_transitions = true

        else
            fail Error::Load,
                 "Can't load resource of type #{resource.class}."
    end

    self
end

#load_delayObject



950
951
952
953
# File 'lib/arachni/browser.rb', line 950

def load_delay
    #(intervals + timeouts).map { |t| t[1] }.max
    @javascript.timeouts.compact.map { |t| t[1].to_i }.max
end

#on_fire_event(&block) ⇒ Object



30
# File 'lib/arachni/browser.rb', line 30

advertise :on_fire_event

#on_new_page(&block) ⇒ Object



33
# File 'lib/arachni/browser.rb', line 33

advertise :on_new_page

#on_new_page_with_sink(&block) ⇒ Object



36
# File 'lib/arachni/browser.rb', line 36

advertise :on_new_page_with_sink

#on_response(&block) ⇒ Object



39
# File 'lib/arachni/browser.rb', line 39

advertise :on_response

#page_snapshotsArray<Page>

Returns Page snapshots (stored after events have been fired and JS links clicked) with hashes as keys and pages as values.

Returns:

  • (Array<Page>)

    Page snapshots (stored after events have been fired and JS links clicked) with hashes as keys and pages as values.



776
777
778
# File 'lib/arachni/browser.rb', line 776

def page_snapshots
    @page_snapshots.values
end

#preload(resource) ⇒ Object

Note:

The preloaded resource will be removed once used, for a persistent cache use #cache.

Parameters:



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/arachni/browser.rb', line 279

def preload( resource )
    response =  case resource
                    when HTTP::Response
                        resource

                    when Page
                        resource.response

                    else
                        fail Error::Load,
                             "Can't load resource of type #{resource.class}."
                end

    save_response( response ) if !response.url.include?( request_token )

    @preloads[response.url] = response
    response.url
end

#responseObject



966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
# File 'lib/arachni/browser.rb', line 966

def response
    u = watir.url

    return if skip_path?( u )

    begin
        with_timeout Options.http.request_timeout / 1_000 do
            while !(r = get_response(u))
                sleep 0.1
            end

            fail Timeout::Error if r.timed_out?

            return r
        end
    rescue Timeout::Error
        print_debug "Response for '#{u}' never arrived."
    end

    nil
end

#seleniumSelenium::WebDriver::Driver

Returns Selenium driver interface.

Returns:

  • (Selenium::WebDriver::Driver)

    Selenium driver interface.



990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
# File 'lib/arachni/browser.rb', line 990

def selenium
    return @selenium if @selenium

    client = Selenium::WebDriver::Remote::Http::Typhoeus.new
    client.timeout = WATIR_COM_TIMEOUT

    @selenium = Selenium::WebDriver.for(
        :remote,

        # We need to spawn our own PhantomJS process because Selenium's
        # way sometimes gives us zombies.
        url:                  spawn_browser,
        desired_capabilities: capabilities,
        http_client:          client
    )
end

#shutdownObject



391
392
393
394
395
396
397
398
399
400
# File 'lib/arachni/browser.rb', line 391

def shutdown
    begin
        watir.close if browser_alive?
    rescue Selenium::WebDriver::Error::WebDriverError,
        Watir::Exception::Error
    end

    kill_process
    @proxy.shutdown
end

#skip_path?(path) ⇒ Boolean

Returns:

  • (Boolean)


962
963
964
# File 'lib/arachni/browser.rb', line 962

def skip_path?( path )
    enforce_scope? && super( path )
end

#snapshot_idString

Returns Snapshot ID used to determine whether or not a page snapshot has already been seen. Uses both elements and their DOM events and possible audit workload to determine the ID, as page snapshots should be retained both when further browser analysis can be performed and when new element audit workload (but possibly without any DOM relevance) is available.

Returns:

  • (String)

    Snapshot ID used to determine whether or not a page snapshot has already been seen. Uses both elements and their DOM events and possible audit workload to determine the ID, as page snapshots should be retained both when further browser analysis can be performed and when new element audit workload (but possibly without any DOM relevance) is available.



502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/arachni/browser.rb', line 502

def snapshot_id
    current_url = url

    id = []
    javascript.dom_elements_with_events.each do |element|
        tag_name   = element['tag_name']
        attributes = element['attributes']
        events     = element['events'] +
            Javascript.select_event_attributes( attributes ).to_a
        element_id = attributes['id'].to_s

        case tag_name
            when 'a'
                href        = attributes['href'].to_s
                element_id << href

                if !href.empty?
                    if href.downcase.start_with?( 'javascript:' )
                        events << [ :click, href ]
                    else
                        absolute = to_absolute( href, current_url )
                        next if skip_path?( absolute )

                        events << [ :click, href ]
                    end
                else
                    events << [ :click, current_url ]
                end

            when 'input', 'textarea', 'select'
                events     << [ tag_name.to_sym ]
                element_id << attributes['name'].to_s

            when 'form'
                action      = attributes['action'].to_s
                element_id << "#{action}#{attributes['name']}"

                if !action.empty?
                    if action.downcase.start_with?( 'javascript:' )
                        events << [ :submit, action ]
                    else
                        absolute = to_absolute( action, current_url )
                        if !skip_path?( absolute )
                            events << [ :submit, absolute ]
                        end
                    end
                else
                    events << [ :submit, current_url ]
                end
        end

        next if events.empty?

        id << "#{tag_name}:#{element_id}:#{events}"
    end

    id.uniq.sort.to_s
end

#sourceString

Returns HTML code of the evaluated (DOM/JS/AJAX) page.

Returns:

  • (String)

    HTML code of the evaluated (DOM/JS/AJAX) page.



946
947
948
# File 'lib/arachni/browser.rb', line 946

def source
    watir.html
end

#source_with_line_numbersString

Returns Prefixes each source line with a number.

Returns:

  • (String)

    Prefixes each source line with a number.



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

def source_with_line_numbers
    source.lines.map.with_index do |line, i|
        "#{i+1} - #{line}"
    end.join
end

#start_captureBrowser

Starts capturing requests and parses them into elements of pages, accessible via #captured_pages.

Returns:

See Also:



746
747
748
749
# File 'lib/arachni/browser.rb', line 746

def start_capture
    @capture = true
    self
end

#stop_captureBrowser

Stops the HTTP::Request capture.

Returns:

See Also:



759
760
761
762
# File 'lib/arachni/browser.rb', line 759

def stop_capture
    @capture = false
    self
end

#to_pagePage

Returns Converts the current browser window to a page.

Returns:

  • (Page)

    Converts the current browser window to a page.



789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
# File 'lib/arachni/browser.rb', line 789

def to_page
    if !(r = response)
        return Page.from_data(
            dom: {
                url: watir.url
            },
            response: {
                code: 0,
                url:  url
            }
        )
    end

    page                          = r.to_page
    page.body                     = source
    page.dom.url                  = watir.url
    page.dom.digest               = @javascript.dom_digest
    page.dom.execution_flow_sinks = @javascript.execution_flow_sinks
    page.dom.data_flow_sinks      = @javascript.data_flow_sinks
    page.dom.transitions          = @transitions.dup
    page.dom.skip_states          = skip_states.dup

    # Go through auditable DOM forms and cookies and remove the DOM from
    # them if no events are associated with it.
    #
    # This can save **A LOT** of time during the audit.
    if @javascript.supported?
        if Options.audit.form_doms?
            page.forms.each do |form|
                next if !form.node || !form.dom

                action = form.node['action'].to_s
                form.dom.browser = self

                next if action.downcase.start_with?( 'javascript:' ) ||
                    form.dom.locate.events.any?

                form.skip_dom = true
            end

            page.
            page.clear_cache
        end

        if Options.audit.cookie_doms?
            sinks = @javascript.taint_tracer.data_flow_sinks
            page.cookies.each do |cookie|
                next if sinks.include?( cookie.name ) ||
                    sinks.include?( cookie.value )

                cookie.skip_dom = true
            end

            page.
        end
    end

    page
end

#trigger_event(page, element, event) ⇒ Object

Note:

Captures page #page_snapshots.

Triggers ‘event` on the element described by `tag` on `page`.

Parameters:

  • page (Page)

    Page containing the element’s ‘tag`.

  • element (ElementLocator)
  • event (Symbol)

    Event to trigger.



600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
# File 'lib/arachni/browser.rb', line 600

def trigger_event( page, element, event )
    event = event.to_sym
    transition = fire_event( element, event )

    if !transition
        print_info "Could not trigger '#{event}' on '#{element}' because" <<
            ' the page has changed, capturing a new snapshot.'
        capture_snapshot

        print_info 'Restoring page.'
        restore page
        return
    end

    capture_snapshot( transition )
    restore page
end

#trigger_eventsBrowser

Triggers all events on all elements (once) and captures page snapshots.

Returns:



566
567
568
569
570
571
572
573
574
575
576
# File 'lib/arachni/browser.rb', line 566

def trigger_events
    root_page = to_page

    each_element_with_events do |locator, events|
        events.each do |name, _|
            distribute_event( root_page, locator, name.to_sym )
        end
    end

    self
end

#urlString

Returns Current URL.

Returns:



404
405
406
# File 'lib/arachni/browser.rb', line 404

def url
    normalize_url watir.url
end

#wait_for_timersObject



955
956
957
958
959
960
# File 'lib/arachni/browser.rb', line 955

def wait_for_timers
    delay = load_delay
    return if !delay

    sleep [Options.http.request_timeout, delay].min / 1000.0
end