Restrict notification action links to application paths

This commit is contained in:
2026-07-21 03:16:51 +02:00
parent 92fb409973
commit ae144a13e0
8 changed files with 160 additions and 7 deletions

View File

@@ -18,7 +18,14 @@ from govoplan_notifications.backend.capabilities import dispatch_capability
from govoplan_notifications.backend.db.models import NotificationDeliveryAttempt, NotificationMessage, NotificationPreference
from govoplan_notifications.backend.router import api_get_notification, api_list_notifications, api_notification_summary, api_update_notification
from govoplan_notifications.backend.schemas import NotificationCreateRequest, NotificationPreferencesUpdateRequest, NotificationUpdateRequest
from govoplan_notifications.backend.service import create_notification, deliver_pending, notification_preferences_response, notification_summary, update_notification_preferences
from govoplan_notifications.backend.service import (
create_notification,
deliver_pending,
notification_preferences_response,
notification_response,
notification_summary,
update_notification_preferences,
)
class NotificationServiceTests(unittest.TestCase):
@@ -187,6 +194,53 @@ class NotificationServiceTests(unittest.TestCase):
session.commit()
enqueue.assert_not_called()
def test_action_url_accepts_only_application_relative_paths(self) -> None:
payload = self._inbox_payload(recipient_id="user-1", subject="Safe action")
safe = payload.model_copy(update={"action_url": "/calendar?event=event-1#details"})
self.assertEqual(
NotificationCreateRequest.model_validate(safe.model_dump()).action_url,
"/calendar?event=event-1#details",
)
for action_url in (
"javascript:alert(1)",
"data:text/html,unsafe",
"file:///etc/passwd",
"custom:unsafe",
"https://attacker.example.test/collect",
"//attacker.example.test/collect",
"/\\attacker.example.test/collect",
"/calendar\nmalformed",
):
with self.subTest(action_url=action_url):
with self.assertRaisesRegex(ValueError, "application-relative path"):
NotificationCreateRequest.model_validate(
{**payload.model_dump(), "action_url": action_url}
)
bypassed_validation = NotificationCreateRequest.model_construct(
**{**payload.model_dump(), "action_url": "javascript:alert(1)"}
)
with self.Session() as session:
with self.assertRaisesRegex(ValueError, "application-relative path"):
create_notification(
session,
tenant_id="tenant-1",
payload=bypassed_validation,
)
def test_legacy_unsafe_action_url_is_not_projected_as_a_link(self) -> None:
with self.Session() as session:
notification = create_notification(
session,
tenant_id="tenant-1",
payload=self._inbox_payload(recipient_id="user-1", subject="Legacy action"),
)
notification.action_url = "javascript:alert(1)"
session.flush()
self.assertIsNone(notification_response(notification)["action_url"])
def test_delivery_task_is_enqueued_only_after_notification_commit(self) -> None:
with patch("govoplan_notifications.backend.service._enqueue_celery_delivery") as enqueue:
with self.Session() as session: