Skip to content

Escape user-provided text in fast_html rendering (#888)#922

Open
vjpixel wants to merge 1 commit intodevelopfrom
claude/fix-888-fast-html-escape
Open

Escape user-provided text in fast_html rendering (#888)#922
vjpixel wants to merge 1 commit intodevelopfrom
claude/fix-888-fast-html-escape

Conversation

@vjpixel
Copy link
Copy Markdown
Member

@vjpixel vjpixel commented Apr 25, 2026

fast_html does not escape text or attribute values, yet every as_html() / as_html_thumbnail() output is rendered with | safe in the jinja2 templates — so a malicious title like " onerror="alert(1) or <script>...</script> would inject HTML/JS.

Wrap user-controlled strings (title, author, name, slug, username) with django.utils.html.escape() in Sound, Marker, Object, and Exhibit before passing them to fast_html. Also remove | safe from post.excerpt in post_preview.jinja2excerpt is a plain TextField, so default jinja autoescaping is the correct behavior. (Post.formatted_body is separately sanitized by ProseEditor, so it stays as-is.)

Closes #888

https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB


Generated by Claude Code

fast_html does not escape text content or attribute values, and every
as_html() / as_html_thumbnail() output is rendered with | safe in the
jinja2 templates. User-controlled fields (title, author, name, slug,
username) could therefore inject HTML/JS.

Wrap user strings with django.utils.html.escape before passing them
to fast_html on Sound, Marker, Object, and Exhibit. Also remove the
| safe filter from post.excerpt in post_preview.jinja2 — excerpt is
a plain TextField, so default jinja autoescaping is the correct
behavior (post.formatted_body is separately sanitized by ProseEditor).

Closes #888

https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB
Copy link
Copy Markdown
Member Author

vjpixel commented Apr 25, 2026

Self-review

Verdict: ✅

fast_html does not escape attribute values or text content — verified directly:

>>> render(span('<script>alert(1)</script>'))
'<span><script>alert(1)</script></span>'
>>> render(img(title='<script>alert(1)</script>', src='x'))
'<img title="<script>alert(1)</script>" src="x">'

Combined with | safe on every as_html()/as_html_thumbnail() consumer in jinja2, this was a real attribute-injection / inline-script XSS waiting on someone with a malicious title.

Scope:

  • Sound.as_html/as_html_thumbnail, Marker.as_html, Object.as_html, Exhibit.as_html_thumbnail — all user-controlled strings (title, name, slug, username) wrapped in escape().
  • post.excerpt was rendered with | safe in post_preview.jinja2 despite being a plain TextField| safe removed.
  • post.formatted_body left alone — already sanitized by ProseEditor(sanitize=True).
  • used_in_html_string() and other internal compositions left alone — no user input.

Verified: escape() produces &lt;script&gt;...&lt;/script&gt; in both attribute and text positions, defusing the injection.

Unique: No duplicate PR.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: Missing Input Sanitization

2 participants