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