diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c72e0b..3e39b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ 5.2.0 - 2026-07-20 - Encrypted Microsoft Teams system settings and added a migration for existing plaintext values. +- Added code to harden the URL check for Microsoft Teams 5.1.0 - 2025-05-25 - Replaced inline Microsoft Teams report expiry note with emails notices for client secret expiry. diff --git a/MicrosoftTeams.php b/MicrosoftTeams.php index ca89e89..a19cde7 100644 --- a/MicrosoftTeams.php +++ b/MicrosoftTeams.php @@ -184,6 +184,8 @@ public function validateReportParameters(&$parameters, $reportType) throw new \Exception(Piwik::translate('MicrosoftTeams_IncomingWebhookInvalidErrorMessage')); } + $this->assertWebhookDestinationAllowed($parameters[self::MS_TEAMS_INCOMING_WEBHOOK_URL_PARAMETER]); + $parameters[self::MS_TEAMS_INCOMING_WEBHOOK_URL_PARAMETER] = htmlspecialchars_decode($parameters[self::MS_TEAMS_INCOMING_WEBHOOK_URL_PARAMETER]); } @@ -419,6 +421,8 @@ public function validateCustomAlertReportParameters($parameters, $alertMedium) throw new \Exception(Piwik::translate('MicrosoftTeams_IncomingWebhookInvalidErrorMessage')); } + $this->assertWebhookDestinationAllowed($parameters[self::MS_TEAMS_INCOMING_WEBHOOK_URL_PARAMETER]); + $parameters[self::MS_TEAMS_INCOMING_WEBHOOK_URL_PARAMETER] = htmlspecialchars_decode($parameters[self::MS_TEAMS_INCOMING_WEBHOOK_URL_PARAMETER]); } } @@ -434,6 +438,33 @@ private function isIpHost(?string $host): bool return filter_var($host, FILTER_VALIDATE_IP) !== false || ctype_digit($host); } + private function assertWebhookDestinationAllowed(string $webhookUrl): void + { + $host = parse_url($webhookUrl, PHP_URL_HOST); + if (empty($host)) { + return; + } + + $host = trim($host, '[]'); + + if ($this->isLocalMatomoHost($host)) { + throw new \Exception(Piwik::translate('MicrosoftTeams_IncomingWebhookInvalidErrorMessage')); + } + } + + private function isLocalMatomoHost(string $host): bool + { + $host = strtolower($host); + + if (in_array($host, ['localhost', 'localhost.localdomain', 'ip6-localhost'], true)) { + return true; + } + + $matomoHost = parse_url(SettingsPiwik::getPiwikUrl(), PHP_URL_HOST); + + return !empty($matomoHost) && strcasecmp(trim($matomoHost, '[]'), $host) === 0; + } + /** * * Code to send CustomAlerts via MicrosoftTeams diff --git a/tests/Integration/MicrosoftTeamsTest.php b/tests/Integration/MicrosoftTeamsTest.php index ecd0709..2d4e219 100644 --- a/tests/Integration/MicrosoftTeamsTest.php +++ b/tests/Integration/MicrosoftTeamsTest.php @@ -188,6 +188,49 @@ public function testValidateReportParametersShouldThrowMicrosoftTeamsWebhookUrlE Piwik::postEvent('ScheduledReports.validateReportParameters', [&$parameters, 'teams']); } + public function testValidateReportParametersShouldRejectLocalhostWebhook() + { + $this->setRequiredFields(); + $this->expectException(\Exception::class); + $this->expectExceptionMessage('MicrosoftTeams_IncomingWebhookInvalidErrorMessage'); + $parameters = [ + ScheduledReports::DISPLAY_FORMAT_PARAMETER => ScheduledReports::DISPLAY_FORMAT_GRAPHS_ONLY, + MicrosoftTeams::MS_TEAMS_INCOMING_WEBHOOK_URL_PARAMETER => 'https://localhost/webhook', + ]; + Piwik::postEvent('ScheduledReports.validateReportParameters', [&$parameters, 'teams']); + } + + public function testValidateReportParametersShouldRejectWebhookTargetingMatomoHost() + { + $this->setRequiredFields(); + \Piwik\Option::set('piwikUrl', 'https://victim.example/'); + + $craftedWebhook = 'https://victim.example/wp-admin/admin.php?page=matomo-reporting' + . '&module=API&method=API.get&trigger=archivephp&method=API.getBulkRequest' + . '&urls[]=method%3DPrivacyManager.executeDataPurge'; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('MicrosoftTeams_IncomingWebhookInvalidErrorMessage'); + $parameters = [ + ScheduledReports::DISPLAY_FORMAT_PARAMETER => ScheduledReports::DISPLAY_FORMAT_GRAPHS_ONLY, + MicrosoftTeams::MS_TEAMS_INCOMING_WEBHOOK_URL_PARAMETER => $craftedWebhook, + ]; + Piwik::postEvent('ScheduledReports.validateReportParameters', [&$parameters, 'teams']); + } + + public function testValidateCustomAlertReportParametersShouldRejectWebhookTargetingMatomoHost() + { + $this->setRequiredFields(); + \Piwik\Option::set('piwikUrl', 'https://victim.example/'); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('MicrosoftTeams_IncomingWebhookInvalidErrorMessage'); + $parameters = [ + MicrosoftTeams::MS_TEAMS_INCOMING_WEBHOOK_URL_PARAMETER => 'https://victim.example/wp-admin/admin.php?module=API&method=API.get&trigger=archivephp', + ]; + Piwik::postEvent('CustomAlerts.validateReportParameters', [$parameters, 'teams']); + } + public function testValidateCustomAlertReportParametersShouldThrowMicrosoftTeamsWebhookUrInvalidException() { $this->setRequiredFields(); diff --git a/tests/UI/expected-ui-screenshots/MicrosoftTeams_new_scheduled_reports.png b/tests/UI/expected-ui-screenshots/MicrosoftTeams_new_scheduled_reports.png index 844b2b8..229e973 100644 Binary files a/tests/UI/expected-ui-screenshots/MicrosoftTeams_new_scheduled_reports.png and b/tests/UI/expected-ui-screenshots/MicrosoftTeams_new_scheduled_reports.png differ diff --git a/tests/UI/expected-ui-screenshots/MicrosoftTeams_send_via_teams.png b/tests/UI/expected-ui-screenshots/MicrosoftTeams_send_via_teams.png index 3fc8dce..0c56bfa 100644 Binary files a/tests/UI/expected-ui-screenshots/MicrosoftTeams_send_via_teams.png and b/tests/UI/expected-ui-screenshots/MicrosoftTeams_send_via_teams.png differ diff --git a/tests/UI/expected-ui-screenshots/MicrosoftTeams_send_via_teams_new.png b/tests/UI/expected-ui-screenshots/MicrosoftTeams_send_via_teams_new.png index 71faadb..792a3aa 100644 Binary files a/tests/UI/expected-ui-screenshots/MicrosoftTeams_send_via_teams_new.png and b/tests/UI/expected-ui-screenshots/MicrosoftTeams_send_via_teams_new.png differ diff --git a/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_disabled.png b/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_disabled.png index 3fc8dce..0c56bfa 100644 Binary files a/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_disabled.png and b/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_disabled.png differ diff --git a/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_enabled.png b/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_enabled.png index abb8070..0511950 100644 Binary files a/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_enabled.png and b/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_enabled.png differ diff --git a/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_error.png b/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_error.png index 85c4ffd..919b254 100644 Binary files a/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_error.png and b/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_error.png differ diff --git a/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_pdf_view.png b/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_pdf_view.png index fed41b6..e62b17e 100644 Binary files a/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_pdf_view.png and b/tests/UI/expected-ui-screenshots/MicrosoftTeams_teams_report_pdf_view.png differ