From c158957e182864ce900728f55d9dbc9aded8ef01 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 24 Sep 2026 07:11:06 +0200 Subject: [PATCH] Improve URL address validation --- app/Rules/SafeWebhookUrl.php | 1 + tests/Unit/NotificationWebhookSafetyTest.php | 13 +++++++++++ tests/Unit/SafeWebhookUrlTest.php | 23 ++++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/app/Rules/SafeWebhookUrl.php b/app/Rules/SafeWebhookUrl.php index 28e2531265..5add4c1896 100644 --- a/app/Rules/SafeWebhookUrl.php +++ b/app/Rules/SafeWebhookUrl.php @@ -469,6 +469,7 @@ class SafeWebhookUrl implements ValidationRule '::1/128', '::ffff:0:0/96', '64:ff9b::/96', + '64:ff9b:1::/48', '100::/64', '2001::/23', '2001:2::/48', diff --git a/tests/Unit/NotificationWebhookSafetyTest.php b/tests/Unit/NotificationWebhookSafetyTest.php index 9246c4004a..70e0d3caee 100644 --- a/tests/Unit/NotificationWebhookSafetyTest.php +++ b/tests/Unit/NotificationWebhookSafetyTest.php @@ -2,6 +2,7 @@ use App\Jobs\SendMessageToDiscordJob; use App\Jobs\SendMessageToSlackJob; +use App\Jobs\SendWebhookJob; use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Support\Facades\Http; @@ -34,3 +35,15 @@ it('blocks queued Discord notifications to IPv4-mapped link-local URLs', functio Http::assertNothingSent(); }); + +it('blocks queued webhook, Discord, and Slack sends to NAT64 local-use URLs', function () { + Http::fake(); + + $url = 'http://[64:ff9b:1::7f00:1]/'; + + (new SendWebhookJob(['event' => 'test'], $url))->handle(); + (new SendMessageToDiscordJob(new DiscordMessage('Test', 'Description', DiscordMessage::infoColor()), $url))->handle(); + (new SendMessageToSlackJob(new SlackMessage('Test', 'Description'), $url))->handle(); + + Http::assertNothingSent(); +}); diff --git a/tests/Unit/SafeWebhookUrlTest.php b/tests/Unit/SafeWebhookUrlTest.php index e0fc939431..3c1a9e106e 100644 --- a/tests/Unit/SafeWebhookUrlTest.php +++ b/tests/Unit/SafeWebhookUrlTest.php @@ -327,6 +327,29 @@ it('redacts webhook URLs for logs', function () { ->toBe('https://hooks.slack.com'); }); +it('rejects NAT64 local-use IPv6 literals and DNS answers', function (string $ipv6) { + $url = "http://[{$ipv6}]/webhook"; + $rule = new SafeWebhookUrl(fn (string $host): array => [$ipv6]); + + expect(Validator::make(['url' => $url], ['url' => $rule])->fails())->toBeTrue("Expected literal rejection: {$ipv6}"); + expect(Validator::make(['url' => 'http://nat64.example.test/webhook'], ['url' => $rule])->fails())->toBeTrue("Expected DNS rejection: {$ipv6}"); + expect(fn () => SafeWebhookUrl::httpClientOptions($url))->toThrow(RuntimeException::class, 'unsafe IP address'); + expect(fn () => SafeWebhookUrl::httpClientOptions('http://nat64.example.test/webhook', resolver: fn (string $host): array => [$ipv6])) + ->toThrow(RuntimeException::class, 'unsafe IP address'); +})->with([ + 'loopback' => '64:ff9b:1::7f00:1', + 'private 172.16' => '64:ff9b:1::ac10:1', + 'private 192.168' => '64:ff9b:1::c0a8:1', + 'link-local' => '64:ff9b:1::a9fe:a9fe', +]); + +it('keeps public IPv6 allowed and NAT64 well-known prefix blocked', function () { + $rule = new SafeWebhookUrl; + + expect(Validator::make(['url' => 'https://[2606:4700:4700::1111]/webhook'], ['url' => $rule])->passes())->toBeTrue(); + expect(Validator::make(['url' => 'http://[64:ff9b::7f00:1]/webhook'], ['url' => $rule])->fails())->toBeTrue(); +}); + it('falls back to system DNS when custom DNS returns no answers', function () { InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], [ 'custom_dns_servers' => '1.1.1.1',