Improve and test html sanitisation
This commit is contained in:
parent
0bfec5d046
commit
85ebfef44a
4 changed files with 241 additions and 3 deletions
109
tests/shared_utils/md_test_cases.py
Normal file
109
tests/shared_utils/md_test_cases.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""Read multi-line test cases from markdown files.
|
||||
|
||||
When checking if text coercion is working multiline Python can be very difficult
|
||||
to read and maintain. This file allows the input and expected results to be written
|
||||
into markdown codeblocks.
|
||||
|
||||
Each test is a second level heading.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
# Level 2 markdown heading. Group 1 is the text
|
||||
CASE_REGEX = re.compile(r"^##\s(.*)", re.MULTILINE)
|
||||
# Level 3 markdown heading. Group 1 is the text
|
||||
LEVEL3_REGEX = re.compile(r"^###\s(.*)", re.MULTILINE)
|
||||
# Markdown code block.
|
||||
# Group 1 is the language specification after the back ticks
|
||||
# Group 2 is the code
|
||||
CODE_REGEX = re.compile(
|
||||
r"^```([a-zA-Z]+)?\s?\n(?:(.*?)\n)?```", re.MULTILINE | re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
class MdTestCase:
|
||||
"""A class to represent a test case read from a markdown file.
|
||||
|
||||
Useful attributes:
|
||||
|
||||
* title - The title of the test case
|
||||
* input_code - the input code for the test case
|
||||
* result_code - the expected result code for the test case
|
||||
"""
|
||||
|
||||
def __init__(self, title: str, case_text: str, block_type: str) -> None:
|
||||
"""Initialise the test case.
|
||||
|
||||
:param title: The title of the test case.
|
||||
:param case_text: The full text of the test case.
|
||||
:param block_type: The type of code block expected (the label after the triple
|
||||
backticks).
|
||||
"""
|
||||
self.title = title
|
||||
self.block_type = block_type
|
||||
self.input_code, self.result_code = self._parse(case_text)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return the string representation of the test case."""
|
||||
return f"<MdTestCase: {self.title}>"
|
||||
|
||||
def _parse(self, case_text: str) -> tuple[str, str]:
|
||||
"""Parse the case text and return the input and expected return code."""
|
||||
h3_matches = list(LEVEL3_REGEX.finditer(case_text))
|
||||
n_h3_matches = len(h3_matches)
|
||||
if n_h3_matches != 2:
|
||||
raise ValueError(
|
||||
f"Case {self.title} has {n_h3_matches} level 3 headings. 2 expected."
|
||||
)
|
||||
if h3_matches[0].group(1) != "Input" or h3_matches[1].group(1) != "Result":
|
||||
raise ValueError(
|
||||
f"Case {self.title} does not have `Input` and `Result` headings."
|
||||
)
|
||||
|
||||
input_section = case_text[h3_matches[0].end() : h3_matches[1].start()]
|
||||
result_section = case_text[h3_matches[1].end() : None]
|
||||
|
||||
input_code_blocks = list(CODE_REGEX.finditer(input_section))
|
||||
result_code_blocks = list(CODE_REGEX.finditer(result_section))
|
||||
if len(input_code_blocks) != 1 or len(result_code_blocks) != 1:
|
||||
raise ValueError(
|
||||
f"Case {self.title} must have exactly 1 code block per section."
|
||||
)
|
||||
|
||||
input_code_type = input_code_blocks[0].group(1)
|
||||
input_code = input_code_blocks[0].group(2)
|
||||
result_code_type = result_code_blocks[0].group(1)
|
||||
result_code = result_code_blocks[0].group(2)
|
||||
if input_code_type != self.block_type or result_code_type != self.block_type:
|
||||
raise ValueError(
|
||||
f"Case {self.title}: each code block must be labeled as "
|
||||
f"{self.block_type}"
|
||||
)
|
||||
|
||||
# If the code block is empty Regex returns None. Coerce to empty string.
|
||||
input_code = "" if input_code is None else input_code.strip()
|
||||
result_code = "" if result_code is None else result_code.strip()
|
||||
return input_code, result_code
|
||||
|
||||
|
||||
def find_test_cases(path: str, block_type: str) -> list[MdTestCase]:
|
||||
"""Return all test cases in the markdown file.
|
||||
|
||||
:param path: Path to the markdown file containing cases.
|
||||
:param block_type: The label for each triple backtick code block.
|
||||
|
||||
:return: A list of MdTestCase objects for each case.
|
||||
"""
|
||||
with open(path, "r", encoding="utf-8") as file_obj:
|
||||
text = file_obj.read()
|
||||
test_case_headings = list(CASE_REGEX.finditer(text))
|
||||
test_cases = []
|
||||
n_headings = len(test_case_headings)
|
||||
for i, heading_match in enumerate(test_case_headings):
|
||||
title = heading_match.group(1)
|
||||
start = heading_match.end()
|
||||
end = test_case_headings[i + 1].start() if i + 1 < n_headings else None
|
||||
|
||||
case_text = text[start:end]
|
||||
test_cases.append(MdTestCase(title, case_text, block_type))
|
||||
return test_cases
|
||||
84
tests/unit_tests/data/html_sanitisation_test_cases.md
Normal file
84
tests/unit_tests/data/html_sanitisation_test_cases.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# HTML Sanitisation Test Cases
|
||||
|
||||
This file is in markdown not python for readability.
|
||||
|
||||
Each test is a level 2 heading. The input and expected values are level 3 headings.
|
||||
|
||||
The HTML to test is in triple backtick code blocks.
|
||||
|
||||
Anything not in code blocks is a comment.
|
||||
|
||||
## Test scripts removed
|
||||
|
||||
The script is removed but the contents remain.
|
||||
|
||||
### Input
|
||||
```html
|
||||
<script>alert("x")</script>
|
||||
<p>Hello</p>
|
||||
```
|
||||
### Result
|
||||
```html
|
||||
alert("x")
|
||||
<p>Hello</p>
|
||||
```
|
||||
## Test script in bold tag
|
||||
|
||||
### Input
|
||||
```html
|
||||
<b onmouseover=alert('Wufff!')>click me!</b>
|
||||
```
|
||||
### Result
|
||||
```html
|
||||
<b>click me!</b>
|
||||
```
|
||||
|
||||
## Test script in disallowed tag
|
||||
### Input
|
||||
```html
|
||||
<img src="http://url.to.file.which/not.exist" onerror=alert(document.cookie);>
|
||||
```
|
||||
### Result
|
||||
```html
|
||||
```
|
||||
|
||||
## Test cookie grabber script is escaped
|
||||
The script should become escaped and have no tags
|
||||
### Input
|
||||
```html
|
||||
<SCRIPT type="text/javascript">
|
||||
var adr = '../evil.php?cakemonster=' + escape(document.cookie);
|
||||
</SCRIPT>
|
||||
```
|
||||
### Result
|
||||
```html
|
||||
var adr = '../evil.php?cakemonster=' + escape(document.cookie);
|
||||
```
|
||||
|
||||
## Test with non-http links
|
||||
Remove the javascript from the links.
|
||||
### Input
|
||||
```html
|
||||
<a href="javascript:alert(1)">link</a>
|
||||
<a href="javascript:alert(1)">encoded</a>
|
||||
<a href="mailto:spammy@spam.com">Email</a>
|
||||
<a href="/a/relateive/link">Relative</a>
|
||||
```
|
||||
### Result
|
||||
```html
|
||||
<a>link</a>
|
||||
<a>encoded</a>
|
||||
<a>Email</a>
|
||||
<a>Relative</a>
|
||||
```
|
||||
|
||||
## Test http links become external
|
||||
Remove the javascript from the links.
|
||||
### Input
|
||||
```html
|
||||
<a href="https://openflexure.org">OpenFlexure</a>
|
||||
```
|
||||
### Result
|
||||
```html
|
||||
<a href="https://openflexure.org" target="_blank" rel="noopener noreferrer">OpenFlexure</a>
|
||||
```
|
||||
24
tests/unit_tests/test_ui.py
Normal file
24
tests/unit_tests/test_ui.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""Testing the server specified user interface."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from openflexure_microscope_server import ui
|
||||
|
||||
from ..shared_utils.md_test_cases import find_test_cases
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
HTML_SANITISATION_TEST_FILE = os.path.join(
|
||||
THIS_DIR, "data", "html_sanitisation_test_cases.md"
|
||||
)
|
||||
HTML_SANITISATION_TEST_CASES = find_test_cases(HTML_SANITISATION_TEST_FILE, "html")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("test_case", HTML_SANITISATION_TEST_CASES)
|
||||
def test_html_block_sanitises(test_case):
|
||||
"""Check each case in the test case file sanitises as expected."""
|
||||
sanitised = ui.sanitise_html(test_case.input_code).strip()
|
||||
assert sanitised == test_case.result_code, (
|
||||
f"Test case: '{test_case.title}': Input code doesn't sanitise as expected."
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue