require 'spec_helper'
describe Arachni::Browser::Javascript do
before( :all ) do
@dom_monitor_url = Arachni::Utilities.normalize_url( web_server_url_for( :dom_monitor ) )
@taint_tracer_url = Arachni::Utilities.normalize_url( web_server_url_for( :taint_tracer ) )
end
before( :each ) do
@browser = Arachni::Browser.new
end
after( :each ) do
Arachni::Options.reset
@browser.shutdown
end
subject { @browser.javascript }
describe '#dom_monitor' do
it 'provides access to the DOMMonitor javascript interface' do
@browser.load "#{@taint_tracer_url}/debug"
subject.dom_monitor.js_object.should end_with 'DOMMonitor'
end
end
describe '#taint_tracer' do
it 'provides access to the TaintTracer javascript interface' do
@browser.load "#{@taint_tracer_url}/debug"
subject.taint_tracer.js_object.should end_with 'TaintTracer'
end
end
describe '#custom_code' do
it 'injects the given code into the response' do
subject.custom_code = 'window.has_custom_code = true'
@browser.load "#{@taint_tracer_url}/debug"
subject.run( 'return window.has_custom_code' ).should == true
end
end
describe '#log_execution_flow_sink_stub' do
it 'returns JS code for TaintTracer.log_execution_flow_sink()' do
subject.log_execution_flow_sink_stub( 1, 2, 3 ).should ==
"_#{subject.token}TaintTracer.log_execution_flow_sink(1, 2, 3)"
end
end
describe '#log_data_flow_sink_stub' do
it 'returns JS code for TaintTracer.log_data_flow_sink()' do
subject.log_data_flow_sink_stub( 1, 2, 3 ).should ==
"_#{subject.token}TaintTracer.log_data_flow_sink(1, 2, 3)"
end
end
describe '#debug_stub' do
it 'returns JS code for TaintTracer.debug()' do
subject.debug_stub( 1, 2, 3 ).should ==
"_#{subject.token}TaintTracer.debug(1, 2, 3)"
end
end
describe '#supported?' do
context 'when there is support for the Javascript environment' do
it 'returns true' do
@browser.load "#{@taint_tracer_url}/debug"
subject.supported?.should be_true
end
end
context 'when there is no support for the Javascript environment' do
it 'returns false' do
@browser.load "#{@taint_tracer_url}/without_javascript_support"
subject.supported?.should be_false
end
end
context 'when the resource is out-of-scope' do
it 'returns false' do
Arachni::Options.url = @taint_tracer_url
@browser.load 'http://google.com/'
subject.supported?.should be_false
end
end
end
describe '#log_execution_flow_sink_stub' do
it 'returns JS code that calls JS\'s log_execution_flow_sink_stub()' do
subject.log_execution_flow_sink_stub.should ==
"_#{subject.token}TaintTracer.log_execution_flow_sink()"
@browser.load "#{@taint_tracer_url}/debug?input=#{subject.log_execution_flow_sink_stub}"
@browser.watir.form.submit
subject.execution_flow_sinks.should be_any
subject.execution_flow_sinks.first.data.should be_empty
end
end
describe '#dom_digest' do
it 'returns a string digest of the current DOM tree' do
@browser.load( @dom_monitor_url + 'digest' )
subject.dom_digest.should == subject.dom_monitor.digest
end
end
describe '#dom_elements_with_events' do
it 'returns information about all DOM elements along with their events' do
@browser.load @dom_monitor_url + 'elements_with_events'
subject.dom_elements_with_events.should == [
{ 'tag_name' => 'body', 'events' => [], 'attributes' => {} },
{ 'tag_name' => 'button',
'events' =>
[[:click, 'function (my_button_click) {}'],
[:click, 'function (my_button_click2) {}'],
[:onmouseover, 'function (my_button_onmouseover) {}'],
[:onclick, 'handler_1()']],
'attributes' => { 'onclick' => 'handler_1()', 'id' => 'my-button' } },
{ 'tag_name' => 'button',
'events' =>
[[:click, 'function (my_button2_click) {}'], [:onclick, 'handler_2()']],
'attributes' => { 'onclick' => 'handler_2()', 'id' => 'my-button2' } },
{ 'tag_name' => 'button',
'events' => [[:onclick, 'handler_3()']],
'attributes' => { 'onclick' => 'handler_3()', 'id' => 'my-button3' } }
]
end
end
describe '#timeouts' do
it 'keeps track of setTimeout() timers' do
@browser.load( @dom_monitor_url + 'timeout-tracker' )
subject.timeouts.should == subject.dom_monitor.timeouts
end
end
describe '#intervals' do
it 'keeps track of setInterval() timers' do
@browser.load( @dom_monitor_url + 'interval-tracker' )
subject.intervals.should == subject.dom_monitor.intervals
end
end
describe '#debugging_data' do
it 'returns debugging information' do
@browser.load "#{@taint_tracer_url}/debug?input=#{subject.debug_stub(1)}"
@browser.watir.form.submit
subject.debugging_data.should == subject.taint_tracer.debugging_data
end
end
describe '#execution_flow_sinks' do
it 'returns sink data' do
@browser.load "#{@taint_tracer_url}/debug?input=#{subject.log_execution_flow_sink_stub(1)}"
@browser.watir.form.submit
subject.execution_flow_sinks.should be_any
subject.execution_flow_sinks.should == subject.taint_tracer.execution_flow_sinks
end
end
describe '#data_flow_sinks' do
it 'returns sink data' do
@browser.load "#{@taint_tracer_url}/debug?input=#{subject.log_data_flow_sink_stub( function: { name: 'blah' } )}"
@browser.watir.form.submit
subject.data_flow_sinks.should be_any
subject.data_flow_sinks.should == subject.taint_tracer.data_flow_sinks
end
end
describe '#flush_data_flow_sinks' do
it 'returns sink data' do
@browser.load "#{@taint_tracer_url}/debug?input=#{subject.log_data_flow_sink_stub( function: { name: 'blah' } )}"
@browser.watir.form.submit
sink = subject.flush_data_flow_sinks
sink[0].trace[1].function.arguments[0].delete( 'timeStamp' )
@browser.load "#{@taint_tracer_url}/debug?input=#{subject.log_data_flow_sink_stub( function: { name: 'blah' } )}"
@browser.watir.form.submit
sink2 = subject.taint_tracer.data_flow_sinks
sink2[0].trace[1].function.arguments[0].delete( 'timeStamp' )
sink.should == sink2
end
it 'empties the sink' do
@browser.load "#{@taint_tracer_url}/debug?input=#{subject.log_data_flow_sink_stub}"
@browser.watir.form.submit
subject.flush_data_flow_sinks
subject.data_flow_sinks.should be_empty
end
end
describe '#flush_execution_flow_sinks' do
it 'returns sink data' do
@browser.load "#{@taint_tracer_url}/debug?input=#{subject.log_execution_flow_sink_stub(1)}"
@browser.watir.form.submit
sink = subject.flush_execution_flow_sinks
sink[0].trace[1].function.arguments[0].delete( 'timeStamp' )
@browser.load "#{@taint_tracer_url}/debug?input=#{subject.log_execution_flow_sink_stub(1)}"
@browser.watir.form.submit
sink2 = subject.taint_tracer.execution_flow_sinks
sink2[0].trace[1].function.arguments[0].delete( 'timeStamp' )
sink.should == sink2
end
it 'empties the sink' do
@browser.load "#{@taint_tracer_url}/debug?input=#{subject.log_execution_flow_sink_stub}"
@browser.watir.form.submit
subject.flush_execution_flow_sinks
subject.execution_flow_sinks.should be_empty
end
end
describe '#serve' do
context 'when the request URL is' do
%W(dom_monitor.js taint_tracer.js).each do |filename|
url = "#{described_class::SCRIPT_BASE_URL}#{filename}"
let(:url) { url }
let(:content_type) { 'text/javascript' }
let(:body) do
IO.read( "#{described_class::SCRIPT_LIBRARY}#{filename}" ).
gsub( '_token', "_#{subject.token}" )
end
let(:content_length) { body.bytesize.to_s }
let(:request) { Arachni::HTTP::Request.new( url: url ) }
let(:response) do
Arachni::HTTP::Response.new(
url: url,
request: request
)
end
context url do
before(:each){ subject.serve( request, response ) }
it 'sets the correct status code' do
response.code.should == 200
end
it 'populates the given response body with its contents' do
response.body.should == body
end
it 'sets the correct Content-Type' do
response.headers.content_type.should == content_type
end
it 'sets the correct Content-Length' do
response.headers['content-length'].should == content_length
end
it 'returns true' do
subject.serve( request, response ).should be_true
end
end
end
context 'other' do
it 'returns false' do
request.url = 'stuff'
subject.serve( request, response ).should be_false
end
end
end
end
describe '#inject' do
let(:response) { Arachni::HTTP::Response.new( url: @dom_monitor_url ) }
context 'when the response does not already contain the JS code' do
it 'injects the system\'s JS interfaces in the response body' do
subject.inject( response )
@browser.load response
subject.taint_tracer.initialized.should be_true
subject.dom_monitor.initialized.should be_true
end
it 'updates the Content-Length' do
old_content_length = response.headers['content-length'].to_i
subject.inject( response )
new_content_length = response.headers['content-length'].to_i
new_content_length.should > old_content_length
new_content_length.should == response.body.bytesize
end
it 'returns true' do
subject.inject( response ).should be_true
end
context 'when the response body contains script elements' do
before { response.body = '' }
context 'and a taint has been configured' do
before { subject.taint = 'my_taint' }
it 'injects taint tracer update calls at the top of the script' do
subject.inject( response )
Nokogiri::HTML(response.body).css('script')[-2].to_s.should ==
""
end
it 'injects taint tracer update calls after the script' do
subject.inject( response )
subject.inject( response )
Nokogiri::HTML(response.body).css('script')[-1].to_s.should ==
""
end
end
end
end
context 'when the response already contains the JS code' do
it 'skips it' do
original_response = response.deep_clone
subject.inject( response )
original_response.should_not == response
updated_response = response.deep_clone
subject.inject( response )
updated_response.should == response
end
it 'returns false' do
subject.inject( response ).should be_true
subject.inject( response ).should be_false
end
end
end
describe '#run' do
it 'executes the given script under the browser\'s context' do
@browser.load @dom_monitor_url
Nokogiri::HTML(@browser.source).to_s.should ==
Nokogiri::HTML(subject.run( 'return document.documentElement.innerHTML' ) ).to_s
end
end
end