/* * Copyright 2010-2022 Ecsypno * * This file is part of the Arachni Framework project and is subject to * redistribution and commercial restrictions. Please see the Arachni Framework * web site for more information on licensing and terms of use. */ //if( !window.onerror ) { // window.errors = []; // window.onerror = function() { // window.errors.push( arguments ) // }; //} /* * Allows the system to optimize DOM/JS/AJAX analysis by overriding JS prototypes * and tracking things like bound events and timers. */ var _tokenDOMMonitor = _tokenDOMMonitor || { // Signals that our custom monitoring overrides have already been installed // for this document. initialized: false, event_inheritance_limit: null, // Keeps track of setTimeout() calls. timeouts: [], // Don't include these elements in the `digest` computation. exclude_tags_from_digest: ['P'], // Don't include these attributes in the `digest` computation. exclude_attributes_from_digest: ['data-arachni-id'], // These elements are interesting enough to be considered by // `elements_with_events` even if they don't have any events. allowed_elements_without_events: { "a": true, "input": true, "textarea": true, "select": true, "form": true }, // These elements are allowed to inherit events from their ancestors. allowed_elements_with_inherited_events: { "a": true, "input": true, "textarea": true, "select": true, "form": true, "li": true, "span": true, "button": true }, // These elements should not have events so ignore them. elements_without_events: { "base" : true, "bdo" : true, "br" : true, "head" : true, "html" : true, "iframe" : true, "meta" : true, "param" : true, "script" : true, "style" : true, "title" : true, "link" : true, "hr" : true }, // These events are valid for all elements. universally_valid_events: { "click" : true, "dblclick" : true, "mousedown" : true, "mousemove" : true, "mouseout" : true, "mouseover" : true, "mouseup" : true }, // Valid events for interesting elements, any other events will be ignored. valid_events_per_element:{ "body" : { "load": true }, "form" : { "submit": true, "reset": true }, "input" : { "select": true, "change": true, "focus": true, "blur": true, "keydown": true, "keypress": true, "keyup": true, "input": true }, "textarea" : { "select": true, "change": true, "focus": true, "blur": true, "keydown": true, "keypress": true, "keyup": true, "input": true }, "select" : { "change": true, "focus": true, "blur": true }, "button" : { "focus": true, "blur": true }, "label" : { "focus": true, "blur": true } }, // Generally valid events for when there's no element tag name, like for // Window and Document. valid_events: { "click" : true, "dblclick" : true, "mousedown" : true, "mousemove" : true, "mouseout" : true, "mouseover" : true, "mouseup" : true, "load" : true, "submit" : true, "reset" : true, "select" : true, "change" : true, "focus" : true, "blur" : true, "keydown" : true, "keypress" : true, "keyup" : true, "input" : true }, initialize: function ( event_inheritance_limit ) { if( _tokenDOMMonitor.initialized ) return; _tokenDOMMonitor.event_inheritance_limit = event_inheritance_limit; _tokenDOMMonitor.track_setTimeout(); _tokenDOMMonitor.track_addEventListener(); _tokenDOMMonitor.initialized = true }, update_trackers: function () { }, // Returns information about all DOM elements that have events, along with // some that are interesting enough to be included even without them. // // @param {Number} offset // Start processing elements at the given offset. // @param {Number} batch_size // Max amount of elements to be returned. // Helps keep Selenium response sizes low to keep RAM usage under control // for pages with a large number of elements with events. elements_with_events: function ( offset, batch_size, tag_name_whitelist ) { tag_name_whitelist = tag_name_whitelist || []; var whitelist = {}; for( var f = 0; f < tag_name_whitelist.length; f++ ) { whitelist[tag_name_whitelist[f]] = true; } var events_with_elements = []; var elements = document.getElementsByTagName("*"); var length = elements.length; var global_events = window._arachni_events || []; global_events = global_events.concat( document._arachni_events || [] ); global_events = _tokenDOMMonitor.arrayUnique( global_events ); // Keeps track of the amount of relevant elements (i.e. with events), to // help with the creation of the batch that should be returned based on // `offset` and `batch_size`. var relevant_element_index = 0; var event_inheritance_limit = 0; for( var i = 0; i < length; i++ ) { var element = elements[i]; var tag_name = element.tagName.toLowerCase(); if( _tokenDOMMonitor.is_element_without_events( tag_name ) ) continue; // Pass this element's events down to its descendants. _tokenDOMMonitor.bequeath_events( element ); if( tag_name_whitelist.length > 0 && !whitelist[tag_name] ) continue; // Skip invisible elements. if( !_tokenDOMMonitor.is_visible( element ) ) continue; var e = { tag_name: tag_name, events: element._arachni_events || [], attributes: {} }; // If we haven't reached the event bubbling depth limit and the // current element is allowed to have inherited events, merge them // with its own. if( ( !_tokenDOMMonitor.event_inheritance_limit || event_inheritance_limit < _tokenDOMMonitor.event_inheritance_limit ) && _tokenDOMMonitor.is_allowed_element_with_inherited_events( e.tag_name ) ) { e.events = e.events.concat( element._arachni_inherited_events || [] ); e.events = _tokenDOMMonitor.arrayUnique( e.events.concat( global_events ) ); event_inheritance_limit++; } var attributes = element.attributes; var attr_length = attributes.length; // Extract attributes and events from them. for( var j = 0; j < attr_length; j++ ){ var attr_name = attributes[j].nodeName; // Extract events and handlers from attributes and set them as // element events -- but only if they are appropriate for the // element type. if( _tokenDOMMonitor.is_valid_event_for_element( tag_name, attr_name ) ) { e.events.push( [ attr_name.replace( 'on', '' ), attributes[j].nodeValue ] ) } e.attributes[attr_name] = attributes[j].nodeValue; } // No events and the element isn't interesting enough to be taken // into account without any, skip it. if( !_tokenDOMMonitor.is_allowed_element_without_event( e.tag_name ) && e.events.length == 0 ) { continue } // Group events and their handlers by event type, instead of having // them as independent tuples; and while we're at it do some // normalization too. var grouped_events = {}; for( var k = 0; k < e.events.length; k++ ) { var event = e.events[k]; var event_name = event[0].replace( 'on', '' ); var event_handler = event[1]; // Event type not appropriate for element, we don't know why and // we don't care, we shouldn't waste resources on it. if( !_tokenDOMMonitor.is_valid_event_for_element( tag_name, event_name ) ) { continue; } grouped_events[event_name] = grouped_events[event_name] || []; grouped_events[event_name].push( event_handler.toString() ); } e.events = grouped_events; // Increase the index for the batch of elements that can be returned. relevant_element_index += 1; // If the batch index reached the specified offset allow for the // batch to be created, otherwise ignore the element. if( offset && relevant_element_index <= offset ) continue; events_with_elements.push( e ); // Batch size reached, send it on its way. if( batch_size && events_with_elements.length == batch_size ) return events_with_elements; } // If we got here it means that the current batch didn't reach max size, // just send whatever we managed to collect. return events_with_elements; }, // Digest used to determine whether or not a page has already been seen, // based on the currently available events. // // 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. event_digest: function () { var id = ''; var elements = _tokenDOMMonitor.elements_with_events(); for( var i = 0; i < elements.length; i++ ) { var element = elements[i]; var element_id = ''; switch( element.tag_name ) { case 'a': element_id += element.attributes.href; break; case 'input': case 'textarea': case 'select': element_id += element.attributes.name; break; case 'form': element_id += element.attributes.name; element_id += ','; element_id += element.attributes.action; break; } id += element.tag_name + ':' + element_id + ':' + Object.keys(element.events).join(); id += '-'; } id += 'cookies:' + _tokenDOMMonitor.cookie_names_csv(); return _tokenDOMMonitor.hashCode( id ).toString(); }, cookie_names_csv: function () { var cookies = document.cookie.split(';'); var csv = ''; for( var i = 0; i < cookies.length; i++ ) { csv += cookies[i].split( '=' )[0]; } return csv; }, // Returns an Integer digest of the current DOM tree (i.e. node names and // their attributes without text-nodes). digest: function () { var elements = document.getElementsByTagName("*"); var length = elements.length; var digest = ''; for( var i = 0; i < length; i++ ) { var element = elements[i]; if( _tokenDOMMonitor.exclude_tags_from_digest.indexOf( element.tagName ) > -1 ) continue; digest += '<' + element.tagName; var attributes = element.attributes; var attr_length = attributes.length; for( var j = 0; j < attr_length; j++ ){ if( _tokenDOMMonitor.exclude_attributes_from_digest.indexOf( attributes[j].nodeName ) > -1 ) continue; digest += ' ' + attributes[j].nodeName + '=' + attributes[j].nodeValue; } digest += '>' } return _tokenDOMMonitor.hashCode( digest ); }, // Override setTimeout() so that we'll know to wait for it to be triggered // during DOM analysis to provide sufficient coverage. track_setTimeout: function () { var original_setTimeout = window.setTimeout; window.setTimeout = function() { arguments[1] = parseInt( arguments[1] ); args = []; for( i = 0; i < arguments.length; i++ ) { args[i] = arguments[i]; } args[0] = args[0].toString(); _tokenDOMMonitor.timeouts.push( args ); original_setTimeout.apply( this, arguments ); }; }, // Overrides window.addEventListener and Node.prototype.addEventListener // to intercept event binds so that we can keep track of them in order to // optimize DOM analysis. track_addEventListener: function () { // Override window.addEventListener var original_Window_addEventListener = window.addEventListener; window.addEventListener = function ( event, listener, useCapture ) { _tokenDOMMonitor.registerEvent( window, event, listener ); original_Window_addEventListener.apply( window, [].slice.call( arguments ) ); }; // Override document.addEventListener var original_Document_addEventListener = document.addEventListener; document.addEventListener = function ( event, listener, useCapture ) { _tokenDOMMonitor.registerEvent( document, event, listener ); original_Document_addEventListener.apply( document, [].slice.call( arguments ) ); }; // Override Node.prototype.addEventListener var original_Node_addEventListener = Node.prototype.addEventListener; Node.prototype.addEventListener = function ( event, listener, useCapture ) { _tokenDOMMonitor.registerEvent( this, event, listener ); original_Node_addEventListener.apply( this, [].slice.call( arguments ) ); }; }, // Passes down the element's events to its descendants. bequeath_events: function( element ) { var children = element.childNodes; for( var i = 0; i < children.length; i++ ) { var child = children[i]; if( !('_arachni_inherited_events' in child) ) child['_arachni_inherited_events'] = []; // Merge the element's events with the child's existing inherited ones. if( element['_arachni_events'] ) { child['_arachni_inherited_events'] = element['_arachni_events'].concat( child['_arachni_inherited_events'] ); } // Merge the element's inherited events with the child's existing // inherited ones. if( element['_arachni_inherited_events'] ) { child['_arachni_inherited_events'] = element['_arachni_inherited_events'].concat( child['_arachni_inherited_events'] ); } // Make sure we didn't add duplicates. child['_arachni_inherited_events'] = _tokenDOMMonitor.arrayUnique( child['_arachni_inherited_events'] ) } }, arrayUnique: function( array ) { var a = array.concat(); for( var i = 0; i < a.length; ++i ) { for( var j = i + 1; j < a.length; ++j ) { if( a[i] === a[j] ) a.splice( j--, 1 ); } } return a; }, // Registers an event and its handler for the given element. registerEvent: function ( element, event, handler ) { if( !('_arachni_events' in element) ) element['_arachni_events'] = []; // Custom events are usually in the form of "click.delegateEventsview13". event = event.split( '.' )[0]; // There's no tag name (so we're dealing with Window or Document) and // the event doesn't generally look valid, bail out. if( !element.tagName && !_tokenDOMMonitor.is_valid_event( event ) ) return; // There's a tag name but the event isn't valid for the element type, // bail out. if( element.tagName && !_tokenDOMMonitor.is_valid_event_for_element( element.tagName.toLowerCase(), event ) ) { return; } // All is well, register the event with the element. element['_arachni_events'].push( [event, handler.toString()] ); }, // Sets a unique enough custom ID attribute to elements that lack proper IDs. // This gets called externally (by the Browser) once the page is settled. setElementIds: function() { var elements = document.getElementsByTagName("*"); var length = elements.length; for( var i = 0; i < length; i++ ) { var element = elements[i]; // Window and others don't have attributes. if( typeof( element.getAttribute ) !== 'function' || typeof( element.setAttribute) !== 'function' ) continue; // If the element has an ID we're cool, move on. if( element.getAttribute('id') ) continue; // Skip invisible elements. if( element.offsetWidth <= 0 && element.offsetHeight <= 0 ) continue; // We don't care about elements without events. if( !element._arachni_events || element._arachni_events.length == 0 ) continue; element.setAttribute( 'data-arachni-id', _tokenDOMMonitor.hashCode( element.innerHTML ) ); } }, is_visible: function( element ) { return !!( element.offsetWidth || element.offsetHeight || element.getClientRects().length ); }, is_valid_event: function ( event ) { return Object.prototype.hasOwnProperty.call( _tokenDOMMonitor.valid_events, event.replace( 'on', '' ) ); }, is_valid_event_for_element: function ( tag, event ) { event = event.replace( 'on', '' ); if( Object.prototype.hasOwnProperty.call( _tokenDOMMonitor.universally_valid_events, event )){ return true; } return _tokenDOMMonitor.valid_events_per_element[tag] && Object.prototype.hasOwnProperty.call( _tokenDOMMonitor.valid_events_per_element[tag], event ) }, is_allowed_element_without_event: function ( tag_name ) { return Object.prototype.hasOwnProperty.call( _tokenDOMMonitor.allowed_elements_without_events, tag_name ); }, is_allowed_element_with_inherited_events: function ( tag_name ) { return Object.prototype.hasOwnProperty.call( _tokenDOMMonitor.allowed_elements_with_inherited_events, tag_name ); }, is_element_without_events: function ( tag_name ) { return Object.prototype.hasOwnProperty.call( _tokenDOMMonitor.elements_without_events, tag_name ); }, hashCode: function( str ) { var hash = 0; if( str.length == 0 ) return hash; for( var i = 0; i < str.length; i++ ) { var char = str.charCodeAt( i ); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; } };