Restrict notification action links to application paths
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user