fix: drawer delete-impact modal HTML escapes user-controllable fields (T110.2)
The delete-impact modal is built via raw f-string concatenation from the ImpactReport — item.kind / item.description / report.notes ultimately embed user-controllable content (turn prose, scene timestamps). A turn with prose like "<script>alert(1)</script>" would reach the rendered HTML verbatim. Currently safe (the fields embedded today are bounded strings) but defense-in-depth — wrap with html.escape() so future description changes can't smuggle markup through. Test: tests/test_drawer_phase4.py::test_delete_impact_modal_escapes_user_controllable_strings.
This commit is contained in:
@@ -458,6 +458,42 @@ def test_t98_4_delete_invokes_rewind_and_drops_cascade(client, tmp_path):
|
||||
assert row is None, f"event {ev_id} should have been deleted"
|
||||
|
||||
|
||||
def test_delete_impact_modal_escapes_user_controllable_strings(client, tmp_path):
|
||||
"""T110.2: defense-in-depth — fields embedded in the modal HTML come
|
||||
from event payloads (turn prose, scene timestamps, etc.) which are
|
||||
ultimately user-controllable. Wrap them with ``html.escape`` so a
|
||||
payload like ``<script>alert(1)</script>`` renders as inert text and
|
||||
doesn't leak through into the rendered modal as actual markup.
|
||||
"""
|
||||
db = tmp_path / "test.db"
|
||||
_seed_chat(db)
|
||||
|
||||
# Seed a user_turn whose prose contains an HTML-script payload. The
|
||||
# modal renders ``description = "turn N (you: <prose excerpt>)"`` so
|
||||
# the prose flows verbatim into the cascade list <li>.
|
||||
with open_db(db) as conn:
|
||||
evil_id = append_and_apply(
|
||||
conn,
|
||||
kind="user_turn",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"prose": "<script>alert('xss')</script>",
|
||||
"segments": [],
|
||||
},
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f"/chats/chat_bot_a/drawer/turn/delete-preview/{evil_id}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
|
||||
# Raw <script> must NOT survive into the rendered HTML. The escaped
|
||||
# form (<script>) is what we want to see instead.
|
||||
assert "<script>alert" not in body
|
||||
assert "<script>alert" in body
|
||||
|
||||
|
||||
def test_delete_turn_with_event_id_zero_returns_400(client, tmp_path):
|
||||
"""T110.1: ``event_id <= 0`` is an obvious client error and must NOT
|
||||
silently rewind the entire log via ``after_event_id = -1``. The route
|
||||
|
||||
Reference in New Issue
Block a user