Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions MicrosoftTeams.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}

Expand Down Expand Up @@ -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]);
}
}
Expand All @@ -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
Expand Down
43 changes: 43 additions & 0 deletions tests/Integration/MicrosoftTeamsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/UI/expected-ui-screenshots/MicrosoftTeams_send_via_teams.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading