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:
@@ -1695,6 +1695,22 @@ def test_the_link_board_refuses_to_render_a_script_href(tmp_path):
|
||||
assert "evil.test" in html, "the refused row vanished instead of being shown inert"
|
||||
|
||||
|
||||
def test_the_link_board_refuses_the_backslash_twin_of_protocol_relative(tmp_path):
|
||||
"""heid bug-hunt 2026-09-28, groa: `//evil.test` was refused and
|
||||
`/\\evil.test` was not, but a browser reads a backslash as a slash in an
|
||||
http(s) URL. Same predicate as the docs, same hole, closed once."""
|
||||
b = tmp_path / "links"
|
||||
b.mkdir()
|
||||
b.joinpath("links.md").write_text(
|
||||
"- [twin](/\\evil.test/x) <sub>· rogue · 2026-09-28 10:00</sub>\n"
|
||||
"- [legitimate](https://ok.test/r) <sub>· fine · 2026-09-28 10:01</sub>\n"
|
||||
)
|
||||
c = TestClient(create_app(tmp_path, ttl_hours=24, start_sweeper=False))
|
||||
html = c.get("/b/links/").text
|
||||
assert 'href="https://ok.test/r"' in html
|
||||
assert 'href="/\\evil.test' not in html
|
||||
|
||||
|
||||
def test_the_board_delete_dialog_cannot_be_rewritten_by_a_link_row(tmp_path):
|
||||
"""A board row's description and URL come from any of seventeen agent
|
||||
handles, and they are pasted into a `confirm()` dialog — which is the text
|
||||
|
||||
Reference in New Issue
Block a user