Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/improve autocomplete js #18552

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions w3af/core/controllers/chrome/devtools/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ class ChromeInterfaceException(Exception):

class ChromeInterfaceTimeout(Exception):
pass


class ChromeScriptRuntimeException(Exception):
def __init__(self, message, function_called=None, *args):
if function_called:
message = "function: {}, exception: {}".format(function_called, message)
super(ChromeScriptRuntimeException, self).__init__(message, *args)
pass
2 changes: 1 addition & 1 deletion w3af/core/controllers/chrome/instrumented/frame_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def _on_frame_navigated(self, message):
# URL all the child frames are removed from Chrome, we should remove
# them from our code too to mirror state
if frame:
for child_frame_id, child_frame in frame.child_frames:
for child_frame_id, child_frame in frame.child_frames.items():
child_frame.detach(self)

frame.set_navigated()
Expand Down
51 changes: 32 additions & 19 deletions w3af/core/controllers/chrome/instrumented/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import json

import w3af.core.controllers.output_manager as om
from w3af.core.controllers.chrome.devtools.exceptions import ChromeScriptRuntimeException

from w3af.core.data.parsers.doc.url import URL
from w3af.core.controllers.chrome.instrumented.instrumented_base import InstrumentedChromeBase
Expand Down Expand Up @@ -297,11 +298,20 @@ def dispatch_js_event(self, selector, event_type):

return True

def get_login_forms(self):
def get_login_forms(self, exact_css_selectors):
"""
:param dict exact_css_selectors: Optional parameter containing css selectors
for part of form like username input or login button.
:return: Yield LoginForm instances
"""
result = self.js_runtime_evaluate('window._DOMAnalyzer.getLoginForms()')
func = (
'window._DOMAnalyzer.getLoginForms("{}", "{}")'
)
func = func.format(
exact_css_selectors.get('username_input', '').replace('"', '\\"'),
exact_css_selectors.get('login_button', '').replace('"', '\\"'),
)
result = self.js_runtime_evaluate(func)

if result is None:
raise EventTimeout('The event execution timed out')
Expand All @@ -316,11 +326,20 @@ def get_login_forms(self):

yield login_form

def get_login_forms_without_form_tags(self):
def get_login_forms_without_form_tags(self, exact_css_selectors):
"""
:param dict exact_css_selectors: Optional parameter containing css selectors
for part of form like username input or login button.
:return: Yield LoginForm instances
"""
result = self.js_runtime_evaluate('window._DOMAnalyzer.getLoginFormsWithoutFormTags()')
func = (
'window._DOMAnalyzer.getLoginFormsWithoutFormTags("{}", "{}")'
)
func = func.format(
exact_css_selectors.get('username_input', '').replace('"', '\\"'),
exact_css_selectors.get('login_button', '').replace('"', '\\"'),
)
result = self.js_runtime_evaluate(func)

if result is None:
raise EventTimeout('The event execution timed out')
Expand Down Expand Up @@ -406,9 +425,9 @@ def focus(self, selector):
if result is None:
return None

node_ids = result.get('result', {}).get('nodeIds', None)
node_ids = result.get('result', {}).get('nodeIds')

if node_ids is None:
if not node_ids:
msg = ('The call to chrome.focus() failed.'
' CSS selector "%s" returned no nodes (did: %s)')
args = (selector, self.debugging_id)
Expand Down Expand Up @@ -589,19 +608,13 @@ def js_runtime_evaluate(self, expression, timeout=5):
timeout=timeout)

# This is a rare case where the DOM is not present
if result is None:
return None

if 'result' not in result:
return None

if 'result' not in result['result']:
return None

if 'value' not in result['result']['result']:
return None

return result['result']['result']['value']
runtime_exception = result.get('result', {}).get('exceptionDetails')
if runtime_exception:
raise ChromeScriptRuntimeException(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is new! Are you sure it is handled in all calls to this method? In the past we were returning None and now we raise an exception. Make sure to search all calls for this method and modify error handling appropiately.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I have added few more fixes in this PR.
This works for sure. It's additional (new) exception which is raised when underlying script throws internal JS exception. We still return None in other cases here https://github.com/andresriancho/w3af/pull/18552/files/8d520bc067455089a9fccac9c395f9114f521e8a#diff-4a954d4e5c6123fad237c66f5399b9dfR617

runtime_exception,
function_called=expression
)
return result.get('result', {}).get('result', {}).get('value', None)

def get_js_variable_value(self, variable_name):
"""
Expand Down
72 changes: 64 additions & 8 deletions w3af/core/controllers/chrome/js/dom_analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ var _DOMAnalyzer = _DOMAnalyzer || {
if( !_DOMAnalyzer.eventIsValidForTagName( tag_name, type ) ) return false;

let selector = OptimalSelect.getSingleSelector(element);

// node_type is https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Node_type_constants
_DOMAnalyzer.event_listeners.push({"tag_name": tag_name,
"node_type": element.nodeType,
Expand Down Expand Up @@ -865,6 +865,48 @@ var _DOMAnalyzer = _DOMAnalyzer || {
return false;
},

/**
* This is naive function which takes parentElement (the login form) and
* tries to find username input field within it.
* @param {Node} parentElement - parent element to scope to document.querySelectorAll()
* @param {String} exactSelector - optional CSS selector. If provided prevents
* using standard selectors
* @returns {NodeList} - result of querySelectorAll()
*/
_getUsernameInput(parentElement, exactSelector = '') {
if (exactSelector) {
return document.querySelectorAll(exactSelector, parentElement);
}
result = document.querySelectorAll("input[type='email']", parentElement);
if (!result.length) {
result = document.querySelectorAll("input[type='text']", parentElement);
}
return result;
},

/**
* This is naive function which takes parentElement (the login form) and tries
* to find submit button within it.
* @param {Node} parentElement - parent element to scope to document.querySelectorAll()
* @param {String} exactSelector - optional CSS selector. If provided prevents
* using standard selectors
* @returns {NodeList} - result of querySelectorAll()
*/
_getSubmitButton(parentElement, exactSelector = '') {
if (exactSelector) {
return document.querySelectorAll(exactSelector, parentElement);
}
result = document.querySelectorAll("input[type='submit']", parentElement);
if (!result.length) {
result = document.querySelectorAll("button[type='submit']", parentElement);
}
// Maybe it's just normal button without type="submit"...
if (!result.length) {
result = document.querySelectorAll('button', parentElement);
}
return result;
},

/**
* Return the CSS selector for the login forms which exist in the DOM.
*
Expand All @@ -874,8 +916,12 @@ var _DOMAnalyzer = _DOMAnalyzer || {
* - <input type=text>, and
* - <input type=password>
*
* @param {String} usernameCssSelector - CSS selector for username input. If
* provided we won't try to find username input automatically.
* @param {String} submitButtonCssSelector - CSS selector for submit button. If
* provided we won't try to find submit button autmatically.
*/
getLoginForms: function () {
getLoginForms: function (usernameCssSelector = '', submitButtonCssSelector = '') {
let login_forms = [];

// First we identify the forms with a password field using a descendant Selector
Expand All @@ -898,15 +944,15 @@ var _DOMAnalyzer = _DOMAnalyzer || {
let form = forms[0];

// Finally we confirm that the form has a type=text input
let text_fields = document.querySelectorAll("input[type='text']", form)
let text_fields = this._getUsernameInput(form, usernameCssSelector);

// Zero text fields is most likely a password-only login form
// Two text fields or more is most likely a registration from
// One text field is 99% of login forms
if (text_fields.length !== 1) continue;

// And if there is a submit button I want that selector too
let submit_fields = document.querySelectorAll("input[type='submit']", form)
let submit_fields = this._getSubmitButton(form, submitButtonCssSelector);
let submit_selector = null;

if (submit_fields.length !== 0) {
Expand Down Expand Up @@ -936,8 +982,12 @@ var _DOMAnalyzer = _DOMAnalyzer || {
* - <input type=text>, and
* - <input type=password>
*
* @param {String} usernameCssSelector - CSS selector for username input. If
* provided we won't try to find username input automatically.
* @param {String} submitButtonCssSelector - CSS selector for submit button. If
* provided we won't try to find submit button autmatically.
*/
getLoginFormsWithoutFormTags: function () {
getLoginFormsWithoutFormTags: function (usernameCssSelector = '', submitButtonCssSelector = '') {
let login_forms = [];

// First we identify the password fields
Expand All @@ -962,7 +1012,7 @@ var _DOMAnalyzer = _DOMAnalyzer || {
// go up one more level, and so one.
//
// Find if this parent has a type=text input
let text_fields = document.querySelectorAll("input[type='text']", parent)
let text_fields = this._getUsernameInput(parent, usernameCssSelector);

// Zero text fields is most likely a password-only login form
// Two text fields or more is most likely a registration from
Expand All @@ -974,7 +1024,7 @@ var _DOMAnalyzer = _DOMAnalyzer || {
}

// And if there is a submit button I want that selector too
let submit_fields = document.querySelectorAll("input[type='submit']", parent)
let submit_fields = this._getSubmitButton(parent, submitButtonCssSelector)
let submit_selector = null;

if (submit_fields.length !== 0) {
Expand All @@ -999,6 +1049,12 @@ var _DOMAnalyzer = _DOMAnalyzer || {
return JSON.stringify(login_forms);
},

clickOnSelector(exactSelector) {
let element = document.querySelector(exactSelector);
element.click();
return 'success'
},

sliceAndSerialize: function (filtered_event_listeners, start, count) {
return JSON.stringify(filtered_event_listeners.slice(start, start + count));
},
Expand Down Expand Up @@ -1142,4 +1198,4 @@ var _DOMAnalyzer = _DOMAnalyzer || {

};

_DOMAnalyzer.initialize();
_DOMAnalyzer.initialize();
14 changes: 11 additions & 3 deletions w3af/core/controllers/chrome/login/find_form/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,24 @@ def __init__(self, chrome, debugging_id):
self.chrome = chrome
self.debugging_id = debugging_id

def find_forms(self):
def find_forms(self, css_selectors=None):
"""
:param dict css_selectors: optional dict of css selectors used to find
elements of form (like username input or login button)
:return: Yield forms as they are found by each strategy
"""
if css_selectors:
msg = 'Form finder uses the CSS selectors: "%s" (did: %s)'
args = (css_selectors, self.debugging_id)
om.out.debug(msg % args)

identified_forms = []

for strategy_klass in self.STRATEGIES:
strategy = strategy_klass(self.chrome, self.debugging_id)
strategy = strategy_klass(self.chrome, self.debugging_id, css_selectors)

try:
strategy.prepare()
for form in strategy.find_forms():
if form in identified_forms:
continue
Expand All @@ -55,6 +63,6 @@ def find_forms(self):
except Exception as e:
msg = 'Form finder strategy %s raised exception: "%s" (did: %s)'
args = (strategy.get_name(),
e,
repr(e),
self.debugging_id)
om.out.debug(msg % args)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from w3af.core.controllers.chrome.instrumented.exceptions import EventTimeout


class BaseFindFormStrategy:
def __init__(self, chrome, debugging_id, exact_css_selectors=None):
"""
:param InstrumentedChrome chrome:
:param String debugging_id:
:param dict exact_css_selectors: Optional parameter containing css selectors
for part of form like username input or login button.
"""
self.chrome = chrome
self.debugging_id = debugging_id
self.exact_css_selectors = exact_css_selectors or {}

def prepare(self):
"""
:raises EventTimeout:
Hook called before find_forms()
"""
form_activator_selector = self.exact_css_selectors.get('form_activator')
if form_activator_selector:
func = 'window._DOMAnalyzer.clickOnSelector("{}")'.format(
form_activator_selector.replace('"', '\\"')
)
result = self.chrome.js_runtime_evaluate(func)
if result is None:
raise EventTimeout('The event execution timed out')

def find_forms(self):
raise NotImplementedError

@staticmethod
def get_name():
return 'BaseFindFormStrategy'
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

"""
from w3af.core.controllers.chrome.login.find_form.strategies.base_find_form_strategy import \
BaseFindFormStrategy


class FormTagStrategy(object):
def __init__(self, chrome, debugging_id):
self.chrome = chrome
self.debugging_id = debugging_id
class FormTagStrategy(BaseFindFormStrategy):

def find_forms(self):
"""
Expand All @@ -37,7 +36,7 @@ def _simple_form_with_username_password_submit(self):
"""
:return: Yield forms that have username, password and submit inputs
"""
for login_form in self.chrome.get_login_forms():
for login_form in self.chrome.get_login_forms(self.exact_css_selectors):
yield login_form

@staticmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,21 @@
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

"""
from w3af.core.controllers.chrome.login.find_form.strategies.base_find_form_strategy import \
BaseFindFormStrategy


class PasswordAndParentStrategy(object):
def __init__(self, chrome, debugging_id):
self.chrome = chrome
self.debugging_id = debugging_id
class PasswordAndParentStrategy(BaseFindFormStrategy):

def find_forms(self):
"""
The algorithm is implemented in dom_analyzer.js

:return: Yield forms which are identified by the strategy algorithm
"""
for login_form in self.chrome.get_login_forms_without_form_tags():
for login_form in self.chrome.get_login_forms_without_form_tags(self.exact_css_selectors):
yield login_form

def get_name(self):
@staticmethod
def get_name():
return 'PasswordAndParent'
Loading