Replace custom HTML stripper and sanitize admin UI post rendering (M4 + M5) #43

Closed
opened 2026-06-14 21:42:34 +00:00 by coding-agent-marvin8 · 1 comment
coding-agent-marvin8 commented 2026-06-14 21:42:34 +00:00 (Migrated from codeberg.org)

Two related HTML-security findings from the security audit share the same root dependency fix (nh3) and should be resolved together.

M5 — Custom HTML stripping instead of battle-tested library (curated.py:44)
The hand-rolled _HTMLStripper class (subclassing html.parser.HTMLParser) is used to strip tags when producing plain-text snippets for the curated queue API. Custom HTML parsers are prone to parser-differential vulnerabilities: malformed tags, SVG namespaces, HTML entities, and CDATA sections can bypass naive stripping in ways that nh3 (Rust ammonia binding) handles correctly.

M4 — Fediverse post HTML rendered without sanitization (post_details.html:279, dashboard.html:518)
Post content fetched from external Fediverse servers is rendered in the admin UI with Jinja2's |safe filter, bypassing autoescaping. A malicious or compromised Fediverse instance can serve posts containing arbitrary HTML/JavaScript that executes in the reviewer's browser session — stored XSS with access to session state and API keys.

Two related HTML-security findings from the security audit share the same root dependency fix (nh3) and should be resolved together. **M5** — Custom HTML stripping instead of battle-tested library (curated.py:44) The hand-rolled _HTMLStripper class (subclassing html.parser.HTMLParser) is used to strip tags when producing plain-text snippets for the curated queue API. Custom HTML parsers are prone to parser-differential vulnerabilities: malformed tags, SVG namespaces, HTML entities, and CDATA sections can bypass naive stripping in ways that nh3 (Rust ammonia binding) handles correctly. **M4** — Fediverse post HTML rendered without sanitization (post_details.html:279, dashboard.html:518) Post content fetched from external Fediverse servers is rendered in the admin UI with Jinja2's |safe filter, bypassing autoescaping. A malicious or compromised Fediverse instance can serve posts containing arbitrary HTML/JavaScript that executes in the reviewer's browser session — stored XSS with access to session state and API keys.
coding-agent-marvin8 commented 2026-06-14 21:42:53 +00:00 (Migrated from codeberg.org)

Findings from exploration:

  • _HTMLStripper / _strip_html() lives in src/fenliu/api/curated.py lines 47–60. It is called by _content_snippet() which feeds the CuratedNextResponse snippet field.
  • |safe appears in templates/post_details.html:279 and templates/dashboard.html:518 — both render post.content from the DB (sourced from external Fediverse servers).
  • Templates are rendered via fenliu.templates_env.templates (a Starlette Jinja2Templates instance). Custom filters can be registered on templates.env.filters.
  • Existing test file: tests/test_strip_html.py covers basic _strip_html behaviour (6 tests).

Plan of attack:

  1. Add nh3~=0.2.21 to pyproject.toml dependencies; sync and regenerate pylock.toml.
  2. M5 (TDD): Add parser-differential bypass tests to test_strip_html.py that fail against _HTMLStripper; replace _HTMLStripper + _strip_html() with nh3.clean(content, tags=set()).
  3. M4 (TDD): Write tests confirming script injection is stripped and safe tags survive; add a sanitize_html Jinja2 filter in templates_env.py using nh3.clean() with an allowed-tag allowlist, returning jinja2.Markup; replace |safe with |sanitize_html in both templates.
  4. Update README.md test count (3 occurrences).
  5. Add Resolution sections to Security-Audit-detail.md for M4 and M5; mark both resolved in Security-Audit.md.
**Findings from exploration:** - _HTMLStripper / _strip_html() lives in src/fenliu/api/curated.py lines 47–60. It is called by _content_snippet() which feeds the CuratedNextResponse snippet field. - |safe appears in templates/post_details.html:279 and templates/dashboard.html:518 — both render post.content from the DB (sourced from external Fediverse servers). - Templates are rendered via fenliu.templates_env.templates (a Starlette Jinja2Templates instance). Custom filters can be registered on templates.env.filters. - Existing test file: tests/test_strip_html.py covers basic _strip_html behaviour (6 tests). **Plan of attack:** 1. Add nh3~=0.2.21 to pyproject.toml dependencies; sync and regenerate pylock.toml. 2. M5 (TDD): Add parser-differential bypass tests to test_strip_html.py that fail against _HTMLStripper; replace _HTMLStripper + _strip_html() with nh3.clean(content, tags=set()). 3. M4 (TDD): Write tests confirming script injection is stripped and safe tags survive; add a sanitize_html Jinja2 filter in templates_env.py using nh3.clean() with an allowed-tag allowlist, returning jinja2.Markup; replace |safe with |sanitize_html in both templates. 4. Update README.md test count (3 occurrences). 5. Add Resolution sections to Security-Audit-detail.md for M4 and M5; mark both resolved in Security-Audit.md.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marvin8/dujiangyan#43
No description provided.