History (#5316): - Transitions fold into episodes (steward/alerts/episodes.py): each stretch out of quiet, the worst state it reached, how long it lasted, its worst reading against the rule ('3m 21s (fires above 3m)'), who was told across its changes, and its Ansible runs. The changes themselves fold under it. - An episode cut by paging says so ('cleared later (above)', 'earlier'). - For one rule: a verdict ('Fired 4 times in the last 7d, 1h 2m in all') and no Rule column. Notified and Action show only when the range has any. Tests: - The confirm guard looks for a confirm( call, not the word in the comment that says it is gone (lesson #4365). - The Hosts viewer test looks for the add-host dialog by id, since every page now carries the confirm dialog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
52 lines
2.6 KiB
Python
52 lines
2.6 KiB
Python
"""No template puts a rendered value into inline JavaScript (#5099).
|
|
|
|
An onclick/onsubmit attribute is HTML-decoded before its JavaScript is
|
|
parsed, so autoescaping does not protect a name placed inside it: a monitor
|
|
called x');fetch(…);// ran as script when an admin clicked Delete. A
|
|
prompt goes in data-confirm="…" (base.html asks with it); any other value a
|
|
handler needs is an integer, rendered with |int.
|
|
"""
|
|
import re
|
|
|
|
from tests._templates import REPO as _REPO, template_files as _templates
|
|
|
|
_HANDLER = re.compile(r"""\bon[a-z]+\s*=\s*("[^"]*"|'[^']*')""", re.I)
|
|
_VALUE = re.compile(r"\{\{(.*?)\}\}", re.S)
|
|
|
|
|
|
def unsafe_handler_values(text: str) -> list[str]:
|
|
"""Each {{ … }} inside an inline handler that is not an |int."""
|
|
return [m.group(0) for h in _HANDLER.finditer(text) for m in _VALUE.finditer(h.group(1))
|
|
if not re.search(r"\|\s*int\s*$", m.group(1).strip())]
|
|
|
|
|
|
def test_the_guard_can_fail():
|
|
page = """
|
|
<button onclick="return confirm('Delete rule {{ rule.name }}?')">x</button>
|
|
<form onsubmit='return confirm({{ msg|tojson }})'>
|
|
<button onclick="toggle({{ idx|int }})">ok</button>
|
|
<button data-confirm="Delete {{ rule.name }}?" data-range="{{ key }}">ok</button>
|
|
"""
|
|
assert unsafe_handler_values(page) == ["{{ rule.name }}", "{{ msg|tojson }}"]
|
|
|
|
|
|
def test_no_template_renders_a_value_into_an_inline_handler():
|
|
files = _templates()
|
|
assert len(files) > 100
|
|
found = {str(p.relative_to(_REPO)): hits for p in files if (hits := unsafe_handler_values(p.read_text()))}
|
|
assert not found, f"use data-confirm, or render an integer with |int: {found}"
|
|
|
|
|
|
def test_base_asks_before_a_data_confirm_form_submits():
|
|
"""#5314: in Steward's own dialog, filled as text, never the browser's confirm()."""
|
|
base = (_REPO / "steward" / "templates" / "base.html").read_text()
|
|
js = (_REPO / "steward" / "static" / "confirm.js").read_text()
|
|
assert "filename='confirm.js'" in base and '<dialog id="confirm-dialog"' in base
|
|
assert '<form method="dialog">' in base
|
|
assert re.search(r'<button type="submit"[^>]*value="back"[^>]*autofocus>', base) # Cancel has the focus
|
|
assert re.search(r'<button type="submit"[^>]*btn-danger[^>]*value="go"', base)
|
|
assert "dataset.confirm" in js and "getElementById('confirm-dialog')" in js and "requestSubmit" in js
|
|
assert "textContent = title" in js and "innerHTML" not in js # text, never markup
|
|
assert not re.search(r"\bconfirm\s*\(", js + base) # a call, not the comment saying it is gone
|
|
assert any("data-confirm=" in p.read_text() for p in _templates())
|