| <html> |
| <!-- |
| |
| DOM checker - browser domain context separation validator |
| ---------------------------------------------------------- |
| |
| Authors: Michal Zalewski <lcamtuf@google.com> |
| Filipe Almeida <filipe@google.com> |
| |
| Copyright 2008 by Google Inc. All Rights Reserved. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
| --> |
| |
| <head> |
| <title>DOM checker - browser domain context separation validator</title> |
| |
| <script src="../json2.js"></script> |
| <script src="automation.js"></script> |
| |
| <script src="dom_config.js"></script> |
| |
| <script> |
| |
| var option_long = false; // Run more timing tests? |
| var option_badonly = false; // Report failed tests only? |
| var running_local = false; // Running from file:///? |
| |
| var bad = 0; // Number of failed tests |
| var now_running = 0; // Run-level nesting counter |
| var target_page; // Target page location |
| var same_blank; // Same-domain blank page location |
| var blank_page; // Blank page location |
| |
| var blank_win; // about:blank window handle |
| |
| var init_timer; // Initialization timer |
| var check_timer; // Check execution timer |
| var flip_timer; // Page transition check timer |
| var write_timer; // IPC write test timer |
| |
| var flip_count = 0; // Page transition counter |
| |
| var disable_ipc = false; // IPC abort flag |
| var write_state; // IPC write state |
| var cur_write; // IPC write pointer |
| |
| var ef_loaded = false; // Local file load test flag |
| |
| var test_list = []; // Test schedule |
| var test_size = 0; |
| |
| var test_count = 1; // Test counter |
| |
| var read_list = []; // Test data sets |
| var read_hash = {}; |
| var write_list = []; |
| var write_hash = []; |
| |
| var last_ipc; // Last IPC command |
| var ipc_wait_count; // IPC wait cycle count |
| var ipc_count = 0; // IPC total command count |
| var ipc_state = 2; // Current IPC state |
| var fail_cycles = 0; // IPC failure count |
| |
| var output; // Output container. |
| |
| /* Send a reset command to IPC peer. */ |
| function ipc_reset() { |
| |
| last_ipc = 'RESET'; |
| if (disable_ipc) return; |
| |
| ipc_wait_count = 0; |
| ipc_count++; |
| document.getElementById('ipc_read').src = blank_page + '#RESET'; |
| |
| } |
| |
| |
| /* Send evaluation request to IPC peer. */ |
| function ipc_eval(expr) { |
| |
| last_ipc = 'EVAL(' + expr + ')'; |
| if (disable_ipc) return; |
| |
| ipc_wait_count = 0; |
| ipc_count++; |
| document.getElementById('ipc_read').src = blank_page + '#' + escape(expr); |
| |
| } |
| |
| |
| /* Test for IPC state change, handle errors and timeouts. */ |
| function ipc_changed() { |
| |
| if (disable_ipc) { |
| if (last_ipc == 'RESET') ipc_state = '2'; else ipc_state = '0'; |
| return true; |
| } |
| |
| try { |
| |
| ipc_value = frames[0].frames['ipc_write'].location.hash.substr(1); |
| |
| } catch (e) { |
| |
| fail_cycles++; |
| if (fail_cycles == 500) { |
| |
| /* So, Opera is a bit naughty and won't let us do that. Oh well. */ |
| |
| alert('Unable to get ipc_write frame hash in response to ' + last_ipc + |
| ' (' + ipc_count + ') in state ' + ipc_state + |
| '\nWARNING: Disabling side channel IPC, results may be less accurate!'); |
| |
| disable_ipc = true; |
| |
| } |
| return false; |
| |
| } |
| |
| fail_cycles = 0; |
| |
| if (ipc_value == ipc_state) { |
| ipc_wait_count++; |
| |
| if (ipc_wait_count == 500) |
| alert('Waited more than 500 cycles on IPC command ' + last_ipc + ' (' + |
| ipc_count + ') in state ' + ipc_state); |
| |
| return false; |
| } |
| |
| ipc_state = ipc_value; |
| return true; |
| |
| } |
| |
| |
| /* Point frames to preconfigured locations, initialize other stuff. */ |
| function init_frames() { |
| |
| try { |
| if (main_host == undefined) throw 1; |
| } catch (e) { |
| alert('Config file dom_checker.js could not be loaded. Please check your configuration.'); |
| return; |
| } |
| |
| if (location.protocol == 'http:' && main_host != location.hostname && |
| main_host != location.host) { |
| alert('Config file dom_checker.js specifies main_host that does not match current hostname.\n' + |
| 'Please check your configuration.'); |
| return; |
| } |
| |
| if (location.href.indexOf('/' + main_host + '/') == -1) |
| running_local = true; |
| |
| target_page = 'http://' + alt_host + '/' + alt_dir + '/dom_target_page.html'; |
| document.getElementById('f').src = target_page; |
| |
| same_blank = 'http://' + main_host + '/' + main_dir + '/dom_blank_page.html'; |
| blank_page = 'http://' + alt_host + '/' + alt_dir + '/dom_blank_page.html'; |
| document.getElementById('ipc_read').src = blank_page; |
| |
| init_timer = setInterval('check_frame()',1000); |
| |
| } |
| |
| |
| /* Wait for 'f' location to be updated; this is actually kind of dodgy, |
| but so is onload= behavior when frame location is modified by scripts. */ |
| function check_frame() { |
| |
| try { |
| |
| var x = frames['f'].location.hostname; |
| if (x != main_host) throw 1; |
| |
| } catch (e) { |
| |
| clearInterval(init_timer); |
| init_button(); |
| |
| } |
| |
| } |
| |
| |
| /* Make the page runnable. */ |
| function init_button() { |
| output = document.getElementById('results'); |
| do_tests(); // Start the tests automatically at page load. |
| } |
| |
| |
| /* Shorthand notation... */ |
| function E(x) { return eval(x); } |
| |
| |
| /* Open a new javascript: window, see if it gets to read window properties to |
| other sites' about:blank windows, and phone back by pointing the window back |
| to our domain. */ |
| function open_blankwin() { |
| |
| /* XXX: Is opener.* the best route to follow? Maybe open('',name) instead? */ |
| |
| try { |
| blank_win = open('javascript:void(location = "' + same_blank + |
| '?" + opener.frames[0].frames[0].location)','blank_win', |
| 'height=100,width=100,location=no,menubar=no,resizable=no,scrollbars=no,status=no,toolbar=no'); |
| } catch (e) { |
| blank_win = open('javascript:void(location = "' + same_blank + |
| '?" + opener.frames[0].frames[0].location)','blank_win', |
| 'height=100,width=100,location=no,menubar=no,resizable=no,scrollbars=no,status=no,toolbar=no'); |
| } |
| |
| } |
| |
| |
| /* See if we get to access that window's properties, and whether it succeeded at |
| retrieving the data. */ |
| function blankwin_verify() { |
| |
| try { |
| var x = blank_win.location.search; |
| if (x.indexOf('about:blank') == -1) throw 1; |
| BAD("about:blank crosstalk"); |
| } catch (e) { |
| GOOD("about:blank crosstalk"); |
| } |
| |
| blank_win.close(); |
| now_running--; |
| |
| } |
| |
| |
| /* Prepare blank window checks... */ |
| function blankwin_checks() { |
| now_running++; |
| setTimeout('blankwin_verify()',2000); |
| } |
| |
| |
| /* Main test entry point. Set up all tests, get the ball rolling. */ |
| function do_tests() { |
| |
| try { |
| if (frames['control_frame'].location.pathname.indexOf('/dom_blank_page.html') == -1) throw 1; |
| } catch (e) { |
| alert('Unable to load control frame - perhaps dom_blank_page.html not present?'); |
| return; |
| } |
| |
| document.getElementById('start').disabled = true; |
| document.getElementById('start').value = "Tests in progress..."; |
| |
| if (document.getElementById('option_badonly').checked) |
| option_badonly = true; |
| |
| if (document.getElementById('option_long').checked) |
| option_long = true; |
| |
| /* We need to call this first, as the ability to open new windows expires pretty |
| quickly after UI events in some browsers */ |
| |
| open_blankwin(); |
| schedule_test('blankwin_checks()'); |
| |
| // First, try some non-destructive ways of stacking odds in our favor... |
| |
| schedule_test('opening_tricks()'); |
| |
| // Try to look up frames by name... |
| |
| schedule_test('name_lookup_test()'); |
| |
| schedule_test('variable_injection_check()'); |
| |
| // Then, try non-intrusive checks on the third-party domain frame |
| |
| schedule_test('basic_checks("frames[0]")'); |
| |
| // Proceed with checks against nested about:blank frame |
| |
| schedule_test('basic_checks("frames[0].frames[0]")'); |
| |
| // Try to bypass IFRAME access checking by going through document.* |
| |
| schedule_test('dom_bypass_checks()'); |
| |
| // Try to perform no-op location updates for that frame (this should be non-disruptive, |
| // and is required because we otherwise blacklist location.* writes). |
| |
| // Temporarily disable these tests to see if it has any effect on |
| // non-determinism. |
| //schedule_test('blank_location_checks_access()'); |
| //schedule_test('blank_location_checks_call()'); |
| |
| // Now that we're done with r/w checks, try destructive overwrites of the nested |
| // about:blank frame we put in the target window: |
| |
| schedule_test('docwrite_checks("frames[0].frames[0]")'); |
| |
| // Let's try the same against the primary target frame itself: |
| |
| schedule_test('docwrite_checks("frames[0]")'); |
| |
| // Try javascript: context inheritance tricks to see if we get to run javascript: |
| // in third party domains. |
| |
| schedule_test('context_checks()'); |
| |
| // Clown around with our own settings for a while to spot more general issues. |
| |
| schedule_test('closing_tricks()'); |
| |
| // Try transition attacks (this is slow and most certainly disruptive). |
| |
| schedule_test('context_switch_checks()'); |
| |
| // Can setTimeout be smuggled across page transitions? |
| |
| schedule_test('timeout_checks()'); |
| |
| // Can IFRAMEs access file:// URLs? |
| |
| if (!running_local) |
| schedule_test('file_frame_checks()'); |
| |
| // That's all, folks... |
| run_tests(); |
| |
| } |
| |
| |
| // Queue a test for running. |
| function schedule_test(cmd) { |
| test_list.push(cmd); |
| } |
| |
| |
| // Begin test execution. |
| function run_tests() { |
| test_size = test_list.length; |
| next_test(); |
| } |
| |
| |
| // Try to see if we get to look up frames by name across sites. |
| function name_lookup_test() { |
| var x; |
| |
| try { |
| x = open('','nf'); |
| if (x == null || x == undefined) throw 1; |
| BAD("open() frame name lookup"); |
| } catch (e) { |
| GOOD("open() frame name lookup"); |
| } |
| |
| } |
| |
| |
| // Fetch test from list, run, schedule next. |
| function next_test() { |
| |
| /* If previous timer-based test is still running, yield. */ |
| |
| if (now_running > 0) { |
| setTimeout(next_test, 100); |
| return; |
| } |
| |
| var cmd = test_list.shift(); |
| var e = document.getElementById('status'); |
| test_count++; |
| |
| if (cmd) { |
| e.innerHTML = "Running test " + (test_count-1) + "/" + test_size + |
| " <font color=gray>(" + cmd + ")</font>"; |
| eval(cmd); |
| setTimeout(next_test, 100); // yield |
| } else { |
| e.innerHTML = ""; |
| update_finalize(); |
| } |
| } |
| |
| |
| function update_finalize() { |
| |
| document.getElementById('start').value = "Testing complete!"; |
| |
| if (bad) |
| document.getElementById('status').innerHTML = |
| "<font color=red>Failed checks: " + bad + "</font>"; |
| else |
| document.getElementById('status').innerHTML = |
| "<font color=green>All checks passed (whoa!)</font>"; |
| |
| // Let the automation know the test is finished. |
| automation.SetDone(); |
| |
| } |
| |
| |
| /* Append a message to test log */ |
| function log(message) { |
| var e = document.createElement('li'); |
| e.innerHTML = message; |
| output.appendChild(e); |
| } |
| |
| |
| /* Log passed test event. */ |
| function GOOD(x) { |
| automation.IncrementTestCount(); |
| |
| if (option_badonly) return; |
| x = x.replace('frames[0].frames[0]','(blank)').replace('frames[0]','(third-party)'); |
| log('<font color=teal>Check passed : ' + x + ' access denied.</font>'); |
| } |
| |
| |
| /* Log test failure. */ |
| function BAD(x) { |
| automation.IncrementTestCount(); |
| |
| bad++; |
| x = x.replace('frames[0].frames[0]','(blank)').replace('frames[0]','(third-party)'); |
| log('<font color=red>CHECK FAILED : ' + x + ' is possible!</font>'); |
| |
| automation.AddFailure(x); |
| } |
| |
| |
| /* For debugging and notification purposes... */ |
| function DEBUG(x) { |
| log('<font color=black>Debug output : ' + x + '</font>'); |
| } |
| |
| |
| /* Let's see if the target page succeeded at planting a variable in our |
| context (this test is actually carried out on load by target page). */ |
| function variable_injection_check() { |
| var message = "(third-party).__defineGetter__('injected_var', ...)"; |
| |
| try { |
| |
| if (injected_var == 1) { } |
| BAD(message); |
| |
| } catch(e) { |
| |
| GOOD(message); |
| |
| } |
| |
| } |
| |
| |
| /* See if we may just set document.domain to whatever we want. */ |
| function opening_tricks() { |
| try { |
| document.domain = alt_host; |
| if (document.domain != alt_host) throw 1; |
| BAD("arbitrary document.domain"); |
| } catch (e) { GOOD("arbitrary document.domain"); } |
| } |
| |
| |
| /* Try some tests that do not involve cross-frame access, but are nevertheless |
| pretty common. */ |
| function closing_tricks() { |
| var x; |
| |
| if (!running_local) { |
| |
| /* NOTE: This requires Apache or IIS server and a generic 'Bad request' |
| response page. If you host the script at a place where these conditions |
| are not met, tough. */ |
| |
| x = new XMLHttpRequest(); |
| try { |
| x.open('GET /? HTTP/1.0\r\nBadHeader\r\nBar: ','http://' + main_host + '/',false); |
| x.send(null); |
| if (x.responseText.indexOf('Bad Request') == -1) throw 1; |
| BAD("XMLHttpRequest method splitting"); |
| } catch (e) { |
| GOOD("XMLHttpRequest method splitting"); |
| } |
| |
| x = new XMLHttpRequest(); |
| try { |
| x.open('GET','http://' + main_host + '/ HTTP/1.0\r\nBadHeader\r\nBar: ',false); |
| x.send(null); |
| if (x.responseText.indexOf('Bad Request') == -1) throw 1; |
| BAD("XMLHttpRequest path splitting"); |
| } catch (e) { |
| GOOD("XMLHttpRequest path splitting"); |
| } |
| |
| x = new XMLHttpRequest(); |
| try { |
| x.open('GET','http://' + main_host + '/',false); |
| try { |
| x.setRequestHeader('Whatever: hi mom\r\nBadHeader\r\nBar','baz'); |
| } catch (e) { |
| x.setRequestHeader('Whatever','hi mom\r\nBadHeader'); |
| } |
| x.send(null); |
| if (x.responseText.indexOf('Bad Request') == -1) throw 1; |
| BAD("XMLHttpRequest parameter splitting"); |
| } catch (e) { |
| GOOD("XMLHttpRequest path splitting"); |
| } |
| |
| // Disabled, causes the page to become unresponsive. See: |
| // http://code.google.com/p/chromium/issues/detail?id=8401 |
| /* |
| x = new XMLHttpRequest(); |
| try { |
| x.open('GET','file:///c:/boot.ini',false); |
| x.send(null); |
| BAD("XMLHttpRequest() to local files (Windows)"); |
| } catch (e) { |
| GOOD("XMLHttpRequest() to local files (Windows)"); |
| } |
| |
| x = new XMLHttpRequest(); |
| try { |
| x.open('GET','file:////etc/hosts',false); |
| x.send(null); |
| BAD("XMLHttpRequest() to local files (unix)"); |
| } catch (e) { |
| GOOD("XMLHttpRequest() to local files (unix)"); |
| } |
| */ |
| |
| } |
| |
| x = new XMLHttpRequest(); |
| try { |
| x.open('GET','http://' + alt_host + '/',false); |
| x.send(null); |
| BAD("XMLHttpRequest() to remote pages"); |
| } catch (e) { |
| GOOD("XMLHttpRequest() to remote pages"); |
| } |
| |
| /* It would be good to test for the ability to set file:/// SRC= URLs, |
| but there is no convenient way to read back the result, I think... |
| onerror= and onload= firing is handy, but very inconsistent across |
| browsers. */ |
| |
| document.cookie = 'dom_checker_cookie=bar_com; path=/; domain=com'; |
| document.cookie = 'dom_checker_cookie=bar_dotcom; path=/; domain=.cx'; |
| document.cookie = 'dom_checker_cookie=bar_dot; path=/; domain=.'; |
| document.cookie = 'dom_checker_cookie=bar_dotcomdot; path=/; domain=.cx.'; |
| |
| // This will overwrite our cookie if domain= was silently dropped on previous |
| // attempts: |
| |
| document.cookie = 'dom_checker_cookie=invalid; path=/'; |
| |
| if (document.cookie.indexOf('dom_checker_cookie=bar') != -1) |
| BAD("cross-domain document.cookie [value: " + document.cookie + "]"); |
| else |
| GOOD("cross-domain document.cookie"); |
| |
| /* Oh, and any cookie setting is evil in file:/// */ |
| |
| if (running_local) { |
| |
| document.cookie = 'dom_checker_cookie2=1'; |
| |
| if (document.cookie.indexOf('dom_checker_cookie2') != -1) |
| BAD("file:/// cookie setting"); |
| else |
| GOOD("file:/// cookie setting"); |
| |
| } |
| |
| /* Depending on the implementation, this might not be a tragic security |
| flaw, as a mutual consent to this is required in most browsers; but it |
| certainly encourages terrible coding practices and may make it easier |
| to go after certain targets. */ |
| |
| try { |
| document.domain = '.cx'; |
| if (document.domain != '.cx') throw 1; |
| BAD("document.domain = '.cx'"); |
| } catch (e) { GOOD("document.domain = '.cx'"); } |
| |
| try { |
| document.domain = 'cx'; |
| if (document.domain != 'cx') throw 1; |
| BAD("document.domain = 'cx'"); |
| } catch (e) { GOOD("document.domain = 'cx'"); } |
| |
| try { |
| document.domain = '.'; |
| if (document.domain != '.') throw 1; |
| BAD("document.domain = '.'"); |
| } catch (e) { GOOD("document.domain = '.'"); } |
| |
| try { |
| document.domain = ''; |
| if (document.domain != '') throw 1; |
| BAD("document.domain = ''"); |
| } catch (e) { GOOD("document.domain = ''"); } |
| |
| } |
| |
| |
| /* Perform basic cross-domain access checks against a specific target. */ |
| function basic_checks(where) { |
| |
| reset_read_write(); |
| |
| /* Brute-force window.* if possible */ |
| |
| iterator_check(where); |
| |
| /* Try to enumerate various list objects */ |
| |
| list_checks(where + ".frames"); |
| list_checks(where + ".window"); /* alias to frames */ |
| list_checks(where + ".images"); |
| list_checks(where + ".styleSheets"); |
| list_checks(where + ".applets"); |
| list_checks(where + ".embeds"); |
| list_checks(where + ".links"); |
| list_checks(where + ".forms"); |
| list_checks(where + ".anchors"); |
| |
| /* Try to call common methods */ |
| |
| call_checks(where + ".document"); |
| call_checks(where + ".document.body"); |
| call_checks(where + ".window"); |
| call_checks(where + ".window.self"); |
| call_checks(where + ".screen"); |
| call_checks(where + ".navigator"); |
| call_checks(where + ".location"); |
| call_checks(where + ".document.location"); |
| call_checks(where + ".history"); |
| |
| visibility_check(where + ".private_var", where + ".var_noexist"); |
| |
| /* Various object-specific calls that are of particular interest */ |
| |
| try_call(where + ".window.scrollBy(10,10)"); |
| try_call(where + ".history.forward(0)"); |
| try_call(where + ".document.createElement('I')"); |
| try_call(where + ".document.body.appendChild(null)"); |
| try_call(where + ".document.clear()"); |
| try_call(where + ".stop()"); |
| |
| /* Some properties that should not be disclosed. */ |
| |
| try_read(where + ".document.location"); |
| try_read(where + ".location"); |
| try_read(where + ".location.href"); |
| try_read(where + ".location.hash"); |
| try_read(where + ".location.protocol"); |
| |
| /* Various object-specific peek & poke attempts to be executed |
| asynchronously. */ |
| |
| add_read_write(where + ".document.domain"); |
| add_read_write(where + ".document.title"); |
| add_read_write(where + ".document.referrer"); |
| add_read_write(where + ".document.URI"); |
| add_read_write(where + ".document.baseURI"); |
| add_read_write(where + ".document.cookie"); |
| add_read_write(where + ".window.name"); |
| add_read_write(where + ".location.search"); |
| add_read_write(where + ".location.host"); |
| add_read_write(where + ".location.hash"); |
| add_write(where + ".location.watch"); |
| add_read_write(where + ".history.length"); |
| add_read_write(where + ".document.style.length"); |
| add_read_write(where + ".document.inputEncoding"); |
| add_read_write(where + ".document.characterSet"); |
| add_read_write(where + ".window.__iterator__"); |
| |
| /* Try to disrupt something! */ |
| |
| fill_read_write("frames['control_frame']", where + "."); |
| fill_read_write("frames['control_frame'].document", where + ".document."); |
| |
| /* Or, how about variable setting? */ |
| |
| add_read_write(where + ".private_var"); |
| |
| /* Actually run tests. */ |
| try_read_write_all(); |
| |
| } |
| |
| |
| /* This is an interesting way to bypass frame[] access checking. */ |
| function dom_bypass_checks() { |
| |
| reset_read_write(); |
| |
| add_read_write("document.getElementById(\'f\').contentDocument.title"); |
| add_read_write("document.getElementById(\'f\').contentWindow.status"); |
| |
| try_read_write_all(); |
| |
| } |
| |
| |
| /* Is it possible to overwrite third-party documents? */ |
| function docwrite_checks(where) { |
| try_call(where + ".document.open()"); |
| try_call(where + ".document.write('hi mom')"); |
| // To prevent clobbering the browser. |
| try { E(where + ".document.close()"); } catch (e) { } |
| } |
| |
| |
| /* Is it possible to move third-party frames? */ |
| function blank_location_checks_access() { |
| |
| reset_read_write(); |
| |
| add_read_write('frames[0].frames[0].location.href','about:blank'); |
| add_read_write('frames[0].frames[0].location','about:blank'); |
| add_read_write('frames[0].frames[0].document.location','about:blank'); |
| |
| try_read_write_all(); |
| |
| } |
| |
| |
| /* Is it possible to move third-party frames, take two? */ |
| function blank_location_checks_call() { |
| try_call("frames[0].frames[0].location.assign('about:blank')"); |
| try_call("frames[0].frames[0].location.replace('about:blank')"); |
| } |
| |
| |
| /* Try to detect context inheritance issues on javascript: URL setting. Note |
| that it differs from earlier *blankwin checks in that it attempts to execute |
| code in a specific context, instead of trying to probe the privileges of |
| blank windows. */ |
| function context_checks() { |
| now_running++; |
| document.getElementById('f').src = blank_page; |
| setTimeout('context_checks_continue()',1000); |
| |
| } |
| |
| |
| /* Continue content inheritance checks... */ |
| function context_checks_continue() { |
| /* We begin with 'f' pointing to a cross-domain (alt_host) site. */ |
| |
| try { |
| document.getElementById('f').src = 'javascript:void(location.href = "' + same_blank + '?" + location.host)'; |
| setTimeout('context_checks_finalize()',1000); |
| } catch (e) { |
| now_running--; |
| GOOD("javascript: URI trickery"); |
| } |
| } |
| |
| |
| /* So, did we succeed at our context inheritance trickery? */ |
| function context_checks_finalize() { |
| var x; |
| try { |
| x = frames['f'].location.search; |
| if (x.indexOf(main_host) != -1) throw 1; |
| if (x == '') throw 1; |
| BAD("javascript: URI trickery [value: " + x + "]"); |
| } catch (e) { GOOD("javascript: URI trickery"); } |
| |
| now_running--; |
| } |
| |
| |
| /* Do we get to test for presence of variables across domains? */ |
| function visibility_check(name, name_noexist) { |
| var exist; |
| var noexist; |
| |
| try { |
| var exist = E("delete " + name); |
| } catch(e) { exist = "exception"; } |
| |
| try { |
| var noexist = E("delete " + name_noexist); |
| } catch(e) { noexist = "exception"; } |
| |
| if(exist == noexist) |
| GOOD("delete " + name + " probe"); |
| else BAD("delete " + name + " probe"); |
| } |
| |
| |
| /* Reset read/write test queue */ |
| function reset_read_write() { |
| read_list = []; |
| read_hash = {}; |
| write_list = []; |
| write_hash = {}; |
| } |
| |
| |
| /* Add read/write test item */ |
| function add_read_write(name) { |
| add_read(name); |
| add_write(name); |
| } |
| |
| |
| /* Add read test item (if not already scheduled) */ |
| function add_read(name) { |
| if(!read_hash[name]) { |
| read_list.push(name); |
| read_hash[name] = 1; |
| } |
| } |
| |
| |
| /* Add write test item (if not already scheduled) */ |
| function add_write(name) { |
| if (!write_hash[name]) { |
| write_list.push(name); |
| write_hash[name] = 1; |
| } |
| } |
| |
| |
| /* Try to iterate through window properties using a control frame, |
| populate lists. */ |
| function fill_read_write(control, base) { |
| for(name in eval(control)) { |
| if (!write_blacklist[name]) add_write(base + name); |
| if (!read_blacklist[name]) add_read(base + name); |
| } |
| } |
| |
| |
| /* Execute read/write tests. Write tests require IPC validation and |
| hence are executed asynchronously. */ |
| function try_read_write_all() { |
| |
| for(i in read_list) |
| try_read(read_list[i]); |
| |
| if (!write_list.length) return; |
| |
| now_running++; |
| cur_write = 0; |
| write_state = 0; |
| write_timer = setInterval('do_next_write()',1); |
| |
| } |
| |
| |
| /* Grab next write item. */ |
| function write_advance() { |
| /* Move to next, end test on EOL */ |
| cur_write++; |
| write_state = 0; |
| |
| if (cur_write == write_list.length) { |
| now_running--; |
| clearInterval(write_timer); |
| return; |
| } |
| } |
| |
| |
| /* Execute next write operation or IPC update. */ |
| function do_next_write() { |
| |
| if (write_state == 0) { |
| |
| /* STATE 0: Issue next command */ |
| |
| if (!try_write_silent(write_list[cur_write],'dom-foo')) { |
| |
| /* Write failed immediately. Report failure, |
| take next item, move to RESET state. */ |
| |
| GOOD(write_list[cur_write] + " write (exception)"); |
| |
| write_advance(); |
| |
| } else { |
| |
| /* Write seemingly succeeded. Is it possible to read the value back? */ |
| |
| if (try_read_silent(write_list[cur_write]).indexOf('dom-foo') != -1) { |
| BAD(write_list[cur_write] + " write (readback)"); |
| write_advance(); |
| } else { |
| |
| /* In local mode, IPC may not be used, because our remote |
| frame will not be able to open file:// URL internally. */ |
| |
| if (running_local) { |
| GOOD(write_list[cur_write] + " write (maybe!)"); |
| write_advance(); |
| return; |
| } |
| |
| /* Otherwise, we must request the target page to revalidate. */ |
| ipc_eval("var tmp = " + write_list[cur_write].replace(/^frames\[0\]\./,'') + "; return (tmp.toString().indexOf('dom-foo') != -1)"); |
| write_state = 1; |
| |
| } |
| |
| } |
| |
| } else if (write_state == 1) { |
| |
| /* STATE 1: Wait for command completion. */ |
| |
| if (!ipc_changed()) return; /* Yield until result is available. */ |
| |
| if (ipc_state == 0) { |
| GOOD(write_list[cur_write] + " write (via IPC)"); |
| } else if (ipc_state == 1) { |
| BAD(write_list[cur_write] + " write (via IPC)"); |
| } else { |
| alert('Bad IPC state ' + ipc_state + ' on eval request'); |
| clearInterval(write_timer); |
| return; |
| } |
| |
| ipc_reset(); |
| write_state = 2; |
| |
| } else if (write_state == 2) { |
| |
| /* STATE 2: Wait for reset, proceed to next. */ |
| |
| if (!ipc_changed()) return; /* Yield until result is available. */ |
| |
| if (ipc_state != 2) { |
| alert('Bad IPC state ' + ipc_state + ' on reset request'); |
| clearInterval(write_timer); |
| return; |
| } |
| |
| write_advance(); |
| |
| } |
| |
| } |
| |
| |
| /* Attempt write; returns 'true' if write *apparently* succeeded. */ |
| function try_write_silent(name,val) { |
| try { E(name + "= '" + val + "'"); return true; } |
| catch (e) { return false; } |
| } |
| |
| |
| /* Attempt read; returns false if name undefined, true otherwise */ |
| function try_read(name) { |
| var x; |
| |
| try { |
| x = E(name); |
| if (x == undefined) return false; |
| // Opera has a magical 'object inaccessible' thingee we need to handle |
| // in a portable manner with an implicit typecast. |
| if (x == '[object inaccessible]') throw 1; |
| BAD(name + " read [value: " + x + "]"); |
| } catch (e) { GOOD(name + " read"); } |
| |
| return true; |
| |
| } |
| |
| |
| /* Attempt read, but do not report. */ |
| function try_read_silent(name) { |
| var x; |
| |
| try { |
| x = E(name); |
| if (x == undefined) throw 1; |
| if (x == '[object inaccessible]') throw 1; |
| return x.toString(); |
| } catch (e) { return "DOM-checker-no-match"; } |
| |
| } |
| |
| |
| /* Try invoking a function. */ |
| function try_call(name) { |
| try { E(name); BAD(name + " call"); } |
| catch (e) { GOOD(name + " call"); } |
| } |
| |
| |
| /* Try to fingerprint object lists across domains. */ |
| function list_checks(name) { |
| var x; |
| |
| try { |
| x = E(name + "[0]"); |
| if (x == undefined) return; |
| BAD(name + "<!-- NOP -->[0] probe [value: " + x + "]"); |
| } catch (e) { GOOD(name + " probe"); } |
| |
| try { |
| x = E(name + ".length"); |
| if (x == undefined) return; |
| BAD(name + ".length read [value: " + x + "]"); |
| } catch (e) { GOOD(name + ".length read"); } |
| |
| iterator_check(name); |
| |
| /* Will be carried out near the end of basic_checks(). */ |
| add_read_write(name + "[0].name"); |
| |
| } |
| |
| |
| /* Call various methods. */ |
| function call_checks(name) { |
| var x; |
| |
| try_call(name + ".hasAttribute('foo')"); |
| try_call(name + ".getAttribute('foo')"); |
| try_call(name + ".setAttribute('foo','bar')"); |
| try_call(name + ".createEvent('MouseEvents')"); |
| try_call(name + ".dispatchEvent(null)"); |
| try_call(name + ".captureEvents(Event.CLICK)"); |
| try_call(name + ".routeEvent(Event.CLICK)"); |
| try_call(name + ".setTimeout('',1)"); |
| try_call(name + ".clearTimeout(0)"); |
| try_call(name + ".watch('foo',function foo(a,b,c){})"); |
| |
| /* These will be executed by the end of basic_checks() */ |
| add_read_write(name + ".onload"); |
| add_read_write(name + ".onerror"); |
| add_read_write(name + ".onchange"); |
| add_read_write(name + ".onkeydown"); |
| |
| } |
| |
| |
| /* Try to enumerate something across domains. */ |
| function iterator_check(name) { |
| try { |
| var list = []; |
| eval("for (e in " + name + ") { list[list.length] = e}"); |
| if(list.length >= 1) { |
| BAD("for (e in " + name + ") iterator"); |
| } else { |
| GOOD("for (e in " + name + ") iterator"); |
| } |
| } catch(e) { GOOD("for (e in " + name + ") iterator listing"); } |
| |
| } |
| |
| |
| /* Finalize page transition checks. */ |
| function flip_finalize() { |
| clearInterval(flip_timer); |
| clearInterval(check_timer); |
| now_running--; |
| } |
| |
| |
| /* Perform a location flip. */ |
| function loc_flip() { |
| // if (Math.random() > .8) return; |
| if (Math.random() > .5) document.getElementById('f').src = blank_page; |
| else document.getElementById('f').src = 'about:blank'; |
| flip_count--; |
| if (!flip_count) { |
| flip_finalize(); |
| GOOD("on-transition cross-domain access"); |
| } |
| } |
| |
| |
| /* Check for location flip success. */ |
| function flip_check() { |
| try { |
| var x = frames['f'].private_var; |
| if (x != 1) throw 1; |
| flip_finalize(); |
| BAD("on-transition private_var access"); |
| } catch (e) { } |
| |
| try { |
| var x = frames['f'].location.hostname; |
| if (x != alt_host) throw 1; |
| flip_finalize(); |
| BAD("on-transition location.hostname access"); |
| } catch (e) { } |
| |
| } |
| |
| |
| /* Prepare for page transition checks */ |
| function context_switch_checks() { |
| now_running++; |
| |
| if (option_long) flip_count = 1000; |
| else flip_count = 100; |
| |
| flip_timer = setInterval('loc_flip()',153); |
| check_timer = setInterval('flip_check()',0); |
| } |
| |
| |
| /* Prepare for timeout checks. */ |
| function timeout_checks() { |
| now_running++; |
| document.getElementById('f').src = same_blank; |
| setTimeout('timeout_load_wait()',1000); |
| } |
| |
| |
| /* Try to configure a timeout across domains. */ |
| function timeout_load_wait() { |
| try { |
| frames['f'].setTimeout('location.href = "' + same_blank +'?" + location.host',1000); |
| frames['f'].navigator.setTimeout('location.href = "' + same_blank +'?" + location.host',1000); |
| frames['f'].screen.setTimeout('location.href = "' + same_blank +'?" + location.host',1000); |
| } catch (e) { } |
| document.getElementById('f').src = blank_page; |
| setTimeout('timeout_result()',2000); |
| } |
| |
| |
| /* Validate test result. */ |
| function timeout_result() { |
| try { |
| var x = frames['f'].location.search; |
| if (x == alt_host) BAD("cross-domain setTimeout"); |
| else GOOD("cross-domain setTimeout [value: " + x + "]") |
| } catch (e) { |
| GOOD("cross-domain setTimeout"); |
| } |
| now_running--; |
| } |
| |
| |
| /* Test callback function. */ |
| function ef_load_ok() { ef_loaded = true; } |
| |
| |
| /* Check the ability to load file:/// URLs in frames. */ |
| function file_frame_checks() { |
| now_running++; |
| ef_loaded = false; |
| |
| try { document.getElementById('ef').src = 'file:///c:/'; } catch(e) { } |
| setTimeout('try { document.getElementById("ef").src = "file:///etc/hosts"} catch(e) { }',500); |
| setTimeout('file_frame_verify()',1000); |
| |
| } |
| |
| |
| /* Test for file:/// load success. */ |
| function file_frame_verify() { |
| now_running--; |
| if (ef_loaded) BAD("file:/// frame"); |
| else GOOD("file:/// frame"); |
| } |
| |
| </script> |
| |
| </head> |
| <body onload="setTimeout('init_frames()',1000)"> |
| |
| <font face="arial"> |
| <font size=+2><b>Browser DOM access checker 1.01</b></font><br> |
| <font size=-1> |
| Authors: Michal Zalewski <<a href="mailto:lcamtuf@google.com">lcamtuf@google.com</a>> and |
| Filipe Almeida <<a href="mailto:filipe@google.com">filipe@google.com</a>><br> |
| Copyright 2008 by Google Inc., and licensed under the Apache License, Version 2.0. |
| <p> |
| <font color=gray> |
| DOM access checker is a tool designed to automatically validate numerous aspects of domain |
| security policy enforcement (cross-domain DOM access, Javascript cookies, XMLHttpRequest |
| calls, event and transition handling) to detect common security attack or information |
| disclosure vectors. |
| <p> |
| Please run this tool both over HTTP, and then from local disk (file:/// namespace). |
| Ideally, results in both cases should be the same, and no failed tests should be reported. |
| That said, although we worked with software vendors to resolve many of the most significant |
| issues, all common browsers fail anywhere from 10 to 30 of less significant tests due to |
| various design decisions (most of which bear some privacy considerations by making it |
| to fingerprint simultaneously open pages). |
| </font> |
| |
| <p> |
| <input type=submit id=start disabled=yes value="Loading, please wait..." onclick="do_tests()"> |
| <span id=status style="padding: 0em 0em 0em 1em"></span> |
| <p> |
| <font size=+1>Test results (be prepared to wait a while):</font><br> |
| <font face="lucida console, courier new"> |
| |
| <!-- Log container --> |
| <div id=results width=100% style="border-width: 1px; border-style: solid; border-color: teal; background-color: #FFFFE0; padding: 1em 0em 1em 1em"> |
| </div> |
| </font> |
| <p> |
| |
| <font size=-1 color=gray> |
| <input id=option_long type=checkbox> Perform longer page transition checks<br> |
| <input id=option_badonly type=checkbox> Report failed checks only |
| </font> |
| <p> |
| |
| <!-- Target frame pointing to dom_target_page.html --> |
| <iframe height=1 width=1 id=f name=f style="border-width: 0px"> |
| </iframe> |
| |
| <!-- Control frame used to enumerate DOM objects --> |
| <iframe height=1 width=1 id=control_frame name=control_frame src="dom_blank_page.html" style="border-width: 0px"> |
| </iframe> |
| |
| <!-- Test frame used for file:/// URLs --> |
| <iframe height=1 width=1 id=ef name=ef onload="ef_load_ok()" style="border-width: 0px"> |
| </iframe> |
| |
| <!-- IPC frame for write validation --> |
| <iframe id=ipc_read name=ipc_read src="dom_blank_page.html#NONE" height=1 width=1 style="border-width: 0px"> |
| </iframe> |
| |
| </body> |
| </html> |