From 4a3857aabb74d82490160aec77a9ebe49f4f4651 Mon Sep 17 00:00:00 2001
From: Chris Coutinho
Date: Thu, 30 Apr 2026 14:17:45 +0200
Subject: [PATCH] fix(webhooks): escape HTML in error responses, compare bearer
as bytes
Address the two Security findings from PR review:
- webhook_receiver: encode Authorization header and expected bearer to
utf-8 bytes before hmac.compare_digest. Conventional form; doesn't
rely on Python's implicit ASCII encoding.
- webhook_routes: html.escape user-influenced and exception-derived
strings before interpolating into HTMLResponse content. Covers the
preset_id path param echoed in the "Unknown preset" branch and the
str(e) text rendered on handler exceptions.
Adds regression tests verifying compare_digest is invoked on bytes and
that "
+
+ with TestClient(app) as client:
+ response = client.post(f"/app/webhooks/enable/{payload}")
+
+ assert response.status_code == 404
+ assert "<script>alert(1)</script>" in response.text
+ assert "" not in response.text
+
+
+def test_disable_unknown_preset_id_is_html_escaped(monkeypatch):
+ _stub_admin_path(monkeypatch)
+ monkeypatch.setattr(webhook_routes, "get_preset", lambda _id: None)
+
+ app = _make_app()
+ payload = ""
+
+ with TestClient(app) as client:
+ response = client.delete(f"/app/webhooks/disable/{payload}")
+
+ assert response.status_code == 404
+ assert "<script>alert(2)</script>" in response.text
+ assert "" not in response.text
+
+
+def test_enable_exception_message_is_html_escaped(monkeypatch):
+ """If the handler raises, the exception text must be escaped before
+ it lands in the 500 response body."""
+
+ async def _boom(_request):
+ raise RuntimeError("
")
+
+ monkeypatch.setattr(webhook_routes, "_get_authenticated_client", _boom)
+
+ app = _make_app()
+
+ with TestClient(app) as client:
+ response = client.post("/app/webhooks/enable/notes_sync")
+
+ assert response.status_code == 500
+ assert "</p><script>x</script>" in response.text
+ assert "" not in response.text
+
+
+def test_disable_exception_message_is_html_escaped(monkeypatch):
+ async def _boom(_request):
+ raise RuntimeError("")
+
+ monkeypatch.setattr(webhook_routes, "_get_authenticated_client", _boom)
+
+ app = _make_app()
+
+ with TestClient(app) as client:
+ response = client.delete("/app/webhooks/disable/notes_sync")
+
+ assert response.status_code == 500
+ assert "</p><script>y</script>" in response.text
+ assert "" not in response.text