@@ -392,7 +392,7 @@ public function test_sub_unsubscribe_success()
392392
393393 $ symfony_request = $ this ->createMock (\phpbb \symfony_request::class);
394394 $ symfony_request ->method ('get ' )->willReturn (json_encode ([
395- 'endpoint ' => 'test_endpoint ' ,
395+ 'endpoint ' => 'https://fcm.googleapis.com/fcm/send/ test_endpoint ' ,
396396 'expiration_time ' => 0 ,
397397 'keys ' => ['p256dh ' => 'test_p256dh ' , 'auth ' => 'test_auth ' ]
398398 ]));
@@ -404,7 +404,7 @@ public function test_sub_unsubscribe_success()
404404
405405 $ this ->assertEquals ([
406406 'user_id ' => '2 ' ,
407- 'endpoint ' => 'test_endpoint ' ,
407+ 'endpoint ' => 'https://fcm.googleapis.com/fcm/send/ test_endpoint ' ,
408408 'p256dh ' => 'test_p256dh ' ,
409409 'auth ' => 'test_auth ' ,
410410 'expiration_time ' => '0 ' ,
@@ -420,6 +420,77 @@ public function test_sub_unsubscribe_success()
420420 $ this ->assertEmpty ($ this ->get_subscription_data ());
421421 }
422422
423+ public function data_subscribe_invalid_endpoint (): array
424+ {
425+ return [
426+ 'no_host ' => ['not_a_url ' ],
427+ 'disallowed_host ' => ['https://evil.example.com/push/endpoint ' ],
428+ 'disallowed_subdomain ' => ['https://evil.fcm.googleapis.com.attacker.com/push ' ],
429+ 'empty_string ' => ['' ],
430+ ];
431+ }
432+
433+ /**
434+ * @dataProvider data_subscribe_invalid_endpoint
435+ */
436+ public function test_subscribe_invalid_endpoint (string $ endpoint ): void
437+ {
438+ $ this ->form_helper ->method ('check_form_tokens ' )->willReturn (true );
439+ $ this ->request ->method ('is_ajax ' )->willReturn (true );
440+ $ this ->user ->data ['user_id ' ] = 2 ;
441+ $ this ->user ->data ['is_bot ' ] = false ;
442+ $ this ->user ->data ['user_type ' ] = USER_NORMAL ;
443+
444+ $ symfony_request = $ this ->createMock (\phpbb \symfony_request::class);
445+ $ symfony_request ->method ('get ' )->willReturn (json_encode ([
446+ 'endpoint ' => $ endpoint ,
447+ 'expiration_time ' => 0 ,
448+ 'keys ' => ['p256dh ' => 'test_p256dh ' , 'auth ' => 'test_auth ' ]
449+ ]));
450+
451+ $ this ->expectException (http_exception::class);
452+ $ this ->expectExceptionMessage ('WEBPUSH_INVALID_ENDPOINT ' );
453+
454+ $ this ->controller ->subscribe ($ symfony_request );
455+ }
456+
457+ public function data_is_valid_endpoint (): array
458+ {
459+ return [
460+ // Exact whitelist matches
461+ ['https://android.googleapis.com/gcm/send/test ' , true ],
462+ ['https://fcm.googleapis.com/fcm/send/test ' , true ],
463+ ['https://updates.push.services.mozilla.com/push/v1/test ' , true ],
464+ ['https://updates-autopush.stage.mozaws.net/push/v1/test ' , true ],
465+ ['https://updates-autopush.dev.mozaws.net/push/v1/test ' , true ],
466+ // Wildcard *.notify.windows.com
467+ ['https://am-0.notify.windows.com/throttledthirdparty/test ' , true ],
468+ ['https://db5.notify.windows.com/push/test ' , true ],
469+ // Wildcard *.push.apple.com
470+ ['https://api.push.apple.com/3/device/test ' , true ],
471+ ['https://api.development.push.apple.com/3/device/test ' , true ],
472+ // Invalid: disallowed host
473+ ['https://evil.example.com/push/endpoint ' , false ],
474+ // Invalid: subdomain spoofing
475+ ['https://evil.fcm.googleapis.com.attacker.com/push ' , false ],
476+ // Invalid: not a URL
477+ ['not_a_url ' , false ],
478+ // Invalid: bare wildcard domain without subdomain
479+ ['https://notify.windows.com/push ' , false ],
480+ ['https://push.apple.com/push ' , false ],
481+ // Invalid: empty string
482+ ['' , false ],
483+ ];
484+ }
485+
486+ /**
487+ * @dataProvider data_is_valid_endpoint
488+ */
489+ public function test_is_valid_endpoint (string $ endpoint , bool $ expected ): void
490+ {
491+ $ this ->assertEquals ($ expected , $ this ->controller ->is_valid_endpoint ($ endpoint ));
492+ }
493+
423494 public function test_toggle_popup_enable_to_disable ()
424495 {
425496 $ this ->form_helper ->method ('check_form_tokens ' )->willReturn (true );
0 commit comments