From 3b5f894435b9b2c38c9710dff6d93db8b559b6c7 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 18 May 2026 18:05:45 -0400 Subject: [PATCH] feat(provenance): nh3 HTML sanitizer for scraped post descriptions Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/utils/html_sanitize.py | 39 ++++++++++++++++++++++++++++++ requirements.txt | 3 +++ tests/test_html_sanitize.py | 38 +++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 backend/app/utils/html_sanitize.py create mode 100644 tests/test_html_sanitize.py diff --git a/backend/app/utils/html_sanitize.py b/backend/app/utils/html_sanitize.py new file mode 100644 index 0000000..d660a5c --- /dev/null +++ b/backend/app/utils/html_sanitize.py @@ -0,0 +1,39 @@ +"""Backend HTML sanitization for scraped Post.description. + +Provenance descriptions come from gallery-dl (untrusted third-party HTML). +We render them via v-html in the UI sub-slice, so they MUST be sanitized +server-side to a tight allowlist. nh3 (Rust/ammonia bindings) is used; +bleach is deprecated upstream and intentionally not used. +""" + +import nh3 + +ALLOWED_TAGS = { + "p", "a", "br", "em", "strong", "b", "i", "ul", "ol", "li", "blockquote", +} +ALLOWED_ATTRIBUTES = {"a": {"href", "title"}} +ALLOWED_URL_SCHEMES = {"http", "https"} + + +def sanitize_post_html(raw: str | None) -> str | None: + """Return sanitized HTML, or None for null/empty/whitespace input. + + - keeps only ALLOWED_TAGS / ALLOWED_ATTRIBUTES + - only http/https hrefs survive (javascript: etc. dropped) + -

body

') + assert "