Fjern inline onsubmit med innholdstekst (XSS i JS-attributt)

Jinja escaper for HTML, men nettleseren dekoder attributtverdien før
JS-parseren ser den, så en tittel med ' kunne bryte ut av strengen.
Bekreftelser går nå via data-confirm og én delt handler i base.html.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcEy43fNYpwg6K6oTKakWR
This commit is contained in:
Ole-Morten Duesund 2026-08-26 12:18:47 +02:00
commit 4794db76b0
4 changed files with 22 additions and 5 deletions

View file

@ -32,5 +32,13 @@
{% endwith %}
{% block content %}{% endblock %}
</main>
<script>
// Bekreftelsesdialoger via data-attributt i stedet for inline onsubmit, slik at
// tekst fra innhold (titler, brukernavn) aldri havner i en JS-kontekst.
document.addEventListener("submit", function (ev) {
var msg = ev.target.dataset && ev.target.dataset.confirm;
if (msg && !confirm(msg)) ev.preventDefault();
});
</script>
</body>
</html>

View file

@ -56,8 +56,7 @@
<li>
<img src="{{ url_for('admin.media', post_id=post.id, filename=name) }}" alt="" loading="lazy">
<code>![]({{ name }})</code>
<form method="post" action="{{ url_for('admin.delete_image', post_id=post.id) }}" class="inline"
onsubmit="return confirm('Slette {{ name }}?')">
<form method="post" action="{{ url_for('admin.delete_image', post_id=post.id) }}" class="inline" data-confirm="Slette {{ name }}?">
<input type="hidden" name="csrf" value="{{ csrf }}">
<input type="hidden" name="filename" value="{{ name }}">
<button type="submit" class="link danger">Slett</button>
@ -68,8 +67,7 @@
</details>
{% endif %}
<form method="post" action="{{ url_for('admin.delete', post_id=post.id) }}" class="danger-zone"
onsubmit="return confirm('Slette hele innlegget «{{ post.title }}»?')">
<form method="post" action="{{ url_for('admin.delete', post_id=post.id) }}" class="danger-zone" data-confirm="Slette hele innlegget «{{ post.title }}»?">
<input type="hidden" name="csrf" value="{{ csrf }}">
<button type="submit" class="link danger">Slett innlegget</button>
</form>

View file

@ -17,7 +17,7 @@
<label>Nytt passord <input type="password" name="password" minlength="8" required autocomplete="new-password"></label>
<button type="submit">Sett passord</button>
</form>
<form method="post" class="inline" onsubmit="return confirm('Slette brukeren {{ u.username }}? Innleggene beholdes i git.')">
<form method="post" class="inline" data-confirm="Slette brukeren {{ u.username }}? Innleggene beholdes i git.">
<input type="hidden" name="csrf" value="{{ csrf }}">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="user_id" value="{{ u.id }}">

View file

@ -146,3 +146,14 @@ def test_subpath_mount(tmp_path):
assert 'href="theme/style.css"' in html
assert re.search(r'href="admin/"', html)
assert client.get("/blog/theme/style.css").status_code == 200
def test_no_inline_js_with_content(client, cfg):
"""Titler med anførselstegn skal ikke havne i en JS-kontekst (XSS)."""
post_id = _create_post(client)
token = csrf(client)
title = "Ola's \"test\" <b>"
r = client.post(f"/admin/edit/{post_id}", data={"csrf": token, "title": title, "body": "x", "action": "save"})
html = client.get(r.headers["Location"]).text
assert "onsubmit" not in html and "onclick" not in html
assert "Ola&#39;s" in html