fix(docs): a posted doc cannot run script on the Booth's origin
Found by design-dev's impeccable run and confirmed at source. Python-Markdown passes raw HTML through, and doc.html and booth.html render the result |safe. A <script> in any session's .md ran on the Booth's origin, and a contract that quoted <pre> opened a real one and swallowed the rest of the doc. Operator ruling: escape raw HTML (not an allowlist). - render_doc deregisters Python-Markdown's block and inline HTML processors, so raw HTML reaches the serializer as text and is escaped there. Fenced and inline code are unchanged. - Every link href in a doc goes through links.is_safe_href after browser-style decoding. Python-Markdown keeps character references in attributes, so `javascript:` reached the browser as `javascript:`. - is_safe_href reads a backslash as a slash, as a browser does in an http(s) URL: `/\evil.test` is `//evil.test`. This also closes the hole on the link board. - A render that raises falls back to raw text, which the template escapes. Two of 19 live .md files render differently. One is a contract losing the quoted <pre> that swallowed it. The other is links.md, which renders as a board, not through render_doc. heid bug-hunt panel (4/4): the core claim held. Its two concrete edges (the backslash twin, the unbounded render) are fixed here. Table tests/mutations/doc_html.toml: 9/9 proved. Suite 951 -> 975.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# A posted doc cannot run code (2026-09-28). design-dev's impeccable run found
|
||||
# raw HTML passing through Python-Markdown into a `|safe` render; operator
|
||||
# ruling: ESCAPE it. Found while fixing it: markdown link hrefs, where an
|
||||
# entity-encoded `javascript:` passes any scheme test that does not decode
|
||||
# it first. Every row is a change tests/test_items.py claims to forbid.
|
||||
#
|
||||
# NOT here, on purpose: the tab/CR/LF drop and the C0 trim in `_browser_href`.
|
||||
# Python 3.13's urlsplit, under `is_safe_href`, drops the same characters, so no
|
||||
# test can see them go. They stay as a statement of browser semantics, and are
|
||||
# not claimed as proven falsifiers.
|
||||
|
||||
unit = "doc html"
|
||||
|
||||
[[mutation]]
|
||||
label = "block-level raw HTML passes through (a <script> block runs)"
|
||||
file = "booth/items.py"
|
||||
test = "tests/test_items.py::test_raw_html_in_a_doc_is_text_never_markup"
|
||||
old = '''
|
||||
md.preprocessors.deregister("html_block")'''
|
||||
new = ''''''
|
||||
|
||||
[[mutation]]
|
||||
label = "inline raw HTML passes through (an <img onerror> runs)"
|
||||
file = "booth/items.py"
|
||||
test = "tests/test_items.py::test_raw_html_in_a_doc_is_text_never_markup"
|
||||
old = '''
|
||||
md.inlinePatterns.deregister("html")'''
|
||||
new = ''''''
|
||||
|
||||
[[mutation]]
|
||||
label = "no href guard at all (javascript: links stay clickable)"
|
||||
file = "booth/items.py"
|
||||
test = "tests/test_items.py::test_a_link_that_would_run_code_keeps_its_text_and_loses_its_href"
|
||||
old = '''
|
||||
md.treeprocessors.register(_UnsafeHrefs(md), "booth_unsafe_hrefs", -10)'''
|
||||
new = ''''''
|
||||
|
||||
[[mutation]]
|
||||
label = "the href guard runs before markdown has written any link"
|
||||
file = "booth/items.py"
|
||||
test = "tests/test_items.py::test_a_link_that_would_run_code_keeps_its_text_and_loses_its_href"
|
||||
old = '''
|
||||
md.treeprocessors.register(_UnsafeHrefs(md), "booth_unsafe_hrefs", -10)'''
|
||||
new = '''
|
||||
md.treeprocessors.register(_UnsafeHrefs(md), "booth_unsafe_hrefs", 30)'''
|
||||
|
||||
[[mutation]]
|
||||
label = "the scheme test reads the raw attribute (javascript: passes)"
|
||||
file = "booth/items.py"
|
||||
test = "tests/test_items.py::test_a_link_that_would_run_code_keeps_its_text_and_loses_its_href"
|
||||
old = '''
|
||||
return _html.unescape(s).translate(_URL_DROPPED).strip(_URL_TRIMMED)'''
|
||||
new = '''
|
||||
return s.translate(_URL_DROPPED).strip(_URL_TRIMMED)'''
|
||||
|
||||
[[mutation]]
|
||||
label = "the guard drops every href, ordinary links included"
|
||||
file = "booth/items.py"
|
||||
test = "tests/test_items.py::test_ordinary_links_survive"
|
||||
old = '''
|
||||
if href is not None and not is_safe_href(_browser_href(href)):'''
|
||||
new = '''
|
||||
if href is not None:'''
|
||||
|
||||
[[mutation]]
|
||||
label = "a backslash is not read as a slash (/\\evil.test passes as a relative path) — docs"
|
||||
file = "booth/links.py"
|
||||
test = "tests/test_items.py::test_a_link_that_leaves_the_origin_by_backslash_is_refused"
|
||||
old = '''
|
||||
parts = urlsplit((url or "").strip().replace("\\", "/"))'''
|
||||
new = '''
|
||||
parts = urlsplit((url or "").strip())'''
|
||||
|
||||
[[mutation]]
|
||||
label = "a backslash is not read as a slash — the board"
|
||||
file = "booth/links.py"
|
||||
test = "tests/test_booth.py::test_the_link_board_refuses_the_backslash_twin_of_protocol_relative"
|
||||
old = '''
|
||||
parts = urlsplit((url or "").strip().replace("\\", "/"))'''
|
||||
new = '''
|
||||
parts = urlsplit((url or "").strip())'''
|
||||
|
||||
[[mutation]]
|
||||
label = "the render is unbounded (a renderer failure raises out of the page)"
|
||||
file = "booth/items.py"
|
||||
test = "tests/test_items.py::test_a_renderer_failure_costs_the_doc_its_formatting_never_the_page"
|
||||
old = '''
|
||||
try:
|
||||
return _markdown_renderer().convert(text), True
|
||||
except Exception: # noqa: BLE001 - deliberate
|
||||
return text, False'''
|
||||
new = '''
|
||||
return _markdown_renderer().convert(text), True'''
|
||||
Reference in New Issue
Block a user