Skip to content
This repository has been archived by the owner on Jan 19, 2024. It is now read-only.

Commit

Permalink
Add the ability to test for XSS vulnerabilities.
Browse files Browse the repository at this point in the history
TNL-4107
  • Loading branch information
cahrens authored and dianakhuang committed Mar 21, 2016
1 parent 244844f commit a3a1145
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 0 deletions.
45 changes: 45 additions & 0 deletions bok_choy/page_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import os
import socket
import urlparse
import re
from textwrap import dedent
from lazy import lazy

Expand All @@ -20,6 +21,15 @@
from .a11y import AxeCoreAudit, AxsAudit


# String that can be used to test for XSS vulnerabilities.
# Taken from https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#XSS_Locator.
XSS_INJECTION = "'';!--\"<XSS>=&{()}"

EXPECTED_INPUT_VALUE_FORMAT = re.compile(r'value="\'\';!--.*<xss.*{\(\)}"')

XSS_HTML = "<xss"


class WrongPageError(Exception):
"""
The page object reports that we're on the wrong page!
Expand All @@ -34,6 +44,13 @@ class PageLoadError(Exception):
pass


class XSSExposureError(Exception):
"""
An XSS issue has been found on the current page.
"""
pass


def unguarded(method):
"""
Mark a PageObject method as unguarded.
Expand Down Expand Up @@ -179,6 +196,8 @@ def __init__(self, browser, *args, **kwargs):
self.browser = browser
a11y_flag = os.environ.get('VERIFY_ACCESSIBILITY', 'False')
self.verify_accessibility = a11y_flag.lower() == 'true'
xss_flag = os.environ.get('VERIFY_XSS', 'False')
self.verify_xss = xss_flag.lower() == 'true'

@lazy
def a11y_audit(self):
Expand Down Expand Up @@ -330,6 +349,30 @@ def _verify_page(self):
)
raise WrongPageError(msg)

def _verify_xss_exposure(self):
"""
Verify that there are no obvious XSS exposures on the page (based on test authors
including XSS_INJECTION in content rendered on the page).
If an xss issue is found, raise a 'XSSExposureError'.
"""
# Use innerHTML to get dynamically injected HTML as well as server-side HTML.
html_source = self.browser.execute_script(
"return document.documentElement.innerHTML.toLowerCase()"
)

# Check taken from https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#XSS_Locator.
all_hits_count = html_source.count(XSS_HTML)
if all_hits_count > 0:
safe_hits_count = len(EXPECTED_INPUT_VALUE_FORMAT.findall(html_source))
if all_hits_count > safe_hits_count:
potential_hits = re.findall('<[^<]+<xss', html_source)
raise XSSExposureError(
"{} XSS issue(s) found on page. Potential places are {}".format(
all_hits_count - safe_hits_count, potential_hits
)
)

@unguarded
def wait_for_page(self, timeout=30):
"""
Expand Down Expand Up @@ -385,6 +428,8 @@ def q(self, **kwargs): # pylint: disable=invalid-name
Returns:
BrowserQuery
"""
if self.verify_xss:
self._verify_xss_exposure()
return BrowserQuery(self.browser, **kwargs)

@contextmanager
Expand Down
100 changes: 100 additions & 0 deletions docs/xss.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
Performing XSS Vulnerability Audits
===================================

The bok-choy framework includes the ability to perform XSS (cross-site scripting) audits on
web pages using a short XSS locator defined in
https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#XSS_Locator.

Trigger XSS Vulnerability Audits in Existing Tests
--------------------------------------------------

You might already have some bok-choy tests written for your web application. To
leverage existing bok-choy tests and have them fail on finding XSS vulnerabilities,
follow these steps.

1. Insert the ``XSS_INJECTION`` string defined in ``bok_choy.page_object`` into your page content.
2. Set the ``VERIFY_XSS`` environment variable to ``True``.

::

export VERIFY_XSS=True


With this environment variable set, an XSS audit is triggered whenever a page object's ``q``
method is called. The audit will detect improper escaping both in HTML and in Javascript
that is embedded within HTML.

If errors are found on the page, an XSSExposureError is raised.

Here is an example of a bok-choy test that will check for XSS vulnerabilities.
It clicks a button on the page, and the user's name is inserted into the page.
If the user name is not properly escaped, the display
of the name (which is data provided by the user and thus potentially malicious) can cause
XSS issues.

In the case of the ``test_button_click_output`` test case in the example below,
an audit will be done in the ``click_button()``, ``output()``, and ``visit()`` method calls,
as each of those will call out to ``q``.

If any XSS errors are found, then the test case will fail with an
XSSExposureError.

.. code-block:: python
from bok_choy.page_object import PageObject, XSS_INJECTION
class MyPage(PageObject):
def url(self):
return 'https://www.mysite.com/page'
def is_browser_on_page(self):
return self.q(css='div#fixture button').present
def click_button(self):
"""
Click on the button element (id="button").
On my example page this will trigger an ajax call
that updates the #output div with the user's name.
"""
self.q(css='div#fixture button').first.click()
self.wait_for_ajax()
@property
def output(self):
"""
Return the contents of the "#output" div on the page.
In the example page, it will contain the user's name after being
updated by the ajax call that is triggered by clicking the button.
"""
text_list = self.q(css='#output').text
if len(text_list) < 1:
return None
else:
return text_list[0]
class MyPageTest(WebAppTest):
def setUp(self):
"""
Log in as a particular user.
"""
super(MyPageTest, self).setUp()
self.user_name = XSS_INJECTION
self.log_in_as_user(self.user_name)
def test_button_click_output(self):
page = MyPage(self.browser)
page.visit()
page.click_button()
self.assertEqual(page.output, self.user_name)
def log_in_as_user(self, user_name):
"""
Would be implemented to log in as a particular user
with a potentially malicious, user-provided name.
"""
pass
12 changes: 12 additions & 0 deletions tests/site/xss_html.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>XSS HTML</title>
</head>
<body>
<div id="fixture">
<div class="unescaped">'';!--\"<XSS>=&{()}</div>
<div>'';!--\"<XSS>=&{()}</div>
</div>
</body>
</html>
12 changes: 12 additions & 0 deletions tests/site/xss_js.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>XSS JS</title>
</head>
<body>
<script>
window.unescaped = "'';!--"<XSS>=&{()}";
</script>
<div id="fixture"/>
</body>
</html>
13 changes: 13 additions & 0 deletions tests/site/xss_mixed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>XSS Mixed</title>
</head>
<body>
<div id="fixture">
<div class="unescaped">'';!--\"<XSS>=&{()}</div>
<div>'';!--\"<XSS>=&{()}</div>
<div class="escaped">&#39;&#39;;!--&#34;&lt;XSS&gt;=&amp;{()}</div>
</div>
</body>
</html>
15 changes: 15 additions & 0 deletions tests/site/xss_safe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>XSS Safe</title>
</head>
<body>
<script>
window.escaped = "\u0027\u0027\u003B!\u002D\u002D\u0022\u003CXSS\u003E\u003D\u0026{()}";
</script>
<div id="fixture">
<div class="escaped">&#39;&#39;;!--&#34;&lt;XSS&gt;=&amp;{()}</div>
<input type="text" value="&#39;&#39;;!--&#34;&lt;XSS&gt;=&amp;{()}">
</div>
</body>
</html>
53 changes: 53 additions & 0 deletions tests/test_xss_exposure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
Tests for identifying XSS vulnerabilities.
This is currently done when the "q" method is called.
"""

import os
from mock import patch

from bok_choy.web_app_test import WebAppTest
from .pages import SitePage
from bok_choy.page_object import XSSExposureError


class XSSExposureTest(WebAppTest):
"""
Tests for identifying XSS vulnerabilities.
"""
def _visit_page(self, page_name):
self.site_page = SitePage(self.browser)
self.site_page.name = page_name
self.site_page.visit()

@patch.dict(os.environ, {'VERIFY_XSS': 'True'})
def test_html_exposure(self):
self._visit_page("xss_html")
with self.assertRaisesRegexp(XSSExposureError, "2 XSS issue"):
self.site_page.q(css='.unescaped')

@patch.dict(os.environ, {'VERIFY_XSS': 'True'})
def test_js_exposure(self):
self._visit_page("xss_js")
with self.assertRaisesRegexp(XSSExposureError, "1 XSS issue"):
self.site_page.q(css='.unescaped')

@patch.dict(os.environ, {'VERIFY_XSS': 'True'})
def test_mixed_exposure(self):
self._visit_page("xss_mixed")
with self.assertRaisesRegexp(XSSExposureError, "2 XSS issue"):
self.site_page.q(css='.unescaped')

@patch.dict(os.environ, {'VERIFY_XSS': 'True'})
def test_escaped(self):
self._visit_page("xss_safe")
self.site_page.q(css='.escaped')

@patch.dict(os.environ, {'VERIFY_XSS': 'False'})
def test_xss_testing_disabled_explicitly(self):
self._visit_page("xss_html")
self.site_page.q(css='.unescaped')

def test_xss_testing_disabled_by_default(self):
self._visit_page("xss_html")
self.site_page.q(css='.unescaped')

0 comments on commit a3a1145

Please sign in to comment.