Ban SafeText-as-child: only Safe nodes render unescaped
Tightens the child model so the type is honest end to end. Previously a ``SafeText``/``mark_safe`` string passed as a child rendered unescaped — a trusted-HTML-as-string backdoor that ``Child = Node | str`` couldn't express (every ``SafeText`` is a ``str``). Now ``_child_key`` escapes *every* string child; the only way to put trusted pre-rendered HTML into the tree is a ``Safe`` node. So a ``str`` child is always untrusted text — which is exactly what the renderer escapes. Converted the trusted-HTML children that relied on the old passthrough: - ``CsrfInput`` and the Alpine selectors (``GameStatusSelector`` / ``SessionDeviceSelector``) now return ``Safe`` nodes instead of ``mark_safe`` strings — they are always tree children. - ``popover_content`` is now a ``Child`` (it is rendered as a child); the one HTML caller (``LinkedPurchase``) passes ``Safe(...)``. - View-side children that were ``mark_safe`` strings → ``Safe(...)``: ``_played_row`` (game detail), the stat SVGs and `` `` spacer (game), the login table (auth), the manual session-form field/label markup (session), and ``_purchase_name`` (stats). - ``SimpleTable.header_action`` typed ``Child``. The script-tag string helpers (``ModuleScript`` / ``StaticScript`` / ``ExternalScript``) stay ``SafeText`` strings: they are only ever joined into the ``scripts=`` string, never used as tree children. ``Children`` regains a bare ``Node`` member (a single node child is valid); the one ``*children`` site (``Popover``) normalises via ``as_children`` first. Tests that asserted the old SafeText-passthrough now assert the new rule (mark_safe child escaped; ``Safe`` node passes through). Full suite green (445; +2 new escaping tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+14
-10
@@ -134,18 +134,20 @@ class Node:
|
||||
return mark_safe(self._render())
|
||||
|
||||
|
||||
# A renderable child is a node or a string (plain strings are escaped, SafeText
|
||||
# and nodes pass through). ``Children`` is the type for a builder's ``children``
|
||||
# A renderable child is a node or a string. Strings are ALWAYS escaped (a string
|
||||
# is untrusted text — ``SafeText``/``mark_safe`` is escaped too); trusted
|
||||
# pre-rendered HTML must be a ``Safe`` node. ``Children`` is the type for a
|
||||
# builder's ``children``
|
||||
# parameter: a sequence of child nodes/strings, a bare string, or nothing. The
|
||||
# sequence is a covariant ``Sequence`` so ``list[Element]`` / ``list[Node]`` are
|
||||
# accepted (a plain ``list[str]`` would be invariant and reject them). A single
|
||||
# bare ``Node`` is accepted only by ``Element`` itself (which wraps it); the
|
||||
# higher-level builders take ``Children``.
|
||||
Child = Node | str
|
||||
Children = Sequence[Child] | str | None
|
||||
Children = Sequence[Child] | Node | str | None
|
||||
|
||||
|
||||
def as_children(children: "Children | Node") -> list[Child]:
|
||||
def as_children(children: Children) -> list[Child]:
|
||||
"""Normalise a builder's ``children`` argument to a flat list.
|
||||
|
||||
Accepts ``None`` (→ empty), a single node/string (→ one-element list), or a
|
||||
@@ -172,16 +174,18 @@ def as_attributes(attributes: "Attributes | None") -> list[HTMLAttribute]:
|
||||
def _child_key(child: object) -> tuple[str, bool]:
|
||||
"""Normalise a child to a ``(text, is_safe)`` pair.
|
||||
|
||||
Nodes render to safe HTML; ``SafeText`` (and anything exposing ``__html__``)
|
||||
is already safe; plain strings are escaped. ``is_safe`` is part of the
|
||||
render cache key so a safe ``"<b>"`` and an unsafe ``"<b>"`` never collide.
|
||||
Only :class:`Node` children render unescaped — that includes :class:`Safe`,
|
||||
the one sanctioned way to put trusted pre-rendered HTML into the tree. Every
|
||||
*string* child is escaped, ``SafeText``/``mark_safe`` included: a string is
|
||||
always treated as untrusted text, so trusted markup must be wrapped in
|
||||
``Safe(...)`` rather than smuggled in as a safe string. ``is_safe`` is part
|
||||
of the render cache key so a safe ``"<b>"`` and an unsafe ``"<b>"`` never
|
||||
collide.
|
||||
"""
|
||||
if isinstance(child, Node):
|
||||
return (child._render(), True)
|
||||
if isinstance(child, str):
|
||||
return (child, isinstance(child, SafeText))
|
||||
if hasattr(child, "__html__"):
|
||||
return (child.__html__(), True)
|
||||
return (child, False)
|
||||
return (str(child), False)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user