fix(validation): improve private URL handling and error feedback

Reject link-local targets even when allowlisted, link private-target errors to endpoint settings, and tighten persistent-volume table widths.
This commit is contained in:
Andras Bacsai committed 2026-08-14 13:34:00 +02:00
1 parent 857375f69c
commit a257224bb7
8 files changed
+126 -185

No files matched your search

@@ -197,7 +197,9 @@ it('renders volumes as a data table with shared column headers', function () {
->toContain('@media (max-width: 768px)')
->toContain('.table-badge-success');
expect($css)->toContain('17.5rem');
expect($css)
->toContain('12rem')
->not->toContain('17.5rem');
// Settings form labels are 13px (not Tailwind text-sm 14px).
expect($css)
+21
View File
@@ -0,0 +1,21 @@
<?php
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View;
use Illuminate\Support\MessageBag;
use Illuminate\Support\ViewErrorBag;
it('renders a trailing validation error URL as a link', function () {
$settingsUrl = route('settings.advanced').'#endpoint-section';
$errors = new ViewErrorBag;
$errors->put('default', new MessageBag([
'endpoint' => "Local or private IP addresses are not allowed. Configure allowed internal targets: {$settingsUrl}",
]));
View::share('errors', $errors);
$html = Blade::render('<x-forms.input id="endpoint" />');
expect($html)
->toContain('href="'.$settingsUrl.'"')
->toContain('Set them here');
});
+39 -1
View File
@@ -1,10 +1,38 @@
<?php
use App\Models\InstanceSettings;
use App\Rules\SafeExternalUrl;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
uses(TestCase::class);
uses(TestCase::class, RefreshDatabase::class);
it('accepts allowlisted private targets', function () {
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], [
'webhook_allowed_internal_hosts' => ['192.168.1.0/24'],
]));
$validator = Validator::make(
['url' => 'http://192.168.1.23/api/v4'],
['url' => new SafeExternalUrl],
);
expect($validator->passes())->toBeTrue();
});
it('rejects allowlisted link-local targets', function () {
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], [
'webhook_allowed_internal_hosts' => ['169.254.0.0/16'],
]));
$validator = Validator::make(
['url' => 'http://169.254.169.254/latest/meta-data'],
['url' => new SafeExternalUrl],
);
expect($validator->fails())->toBeTrue();
});
it('accepts valid public URLs', function () {
$rule = new SafeExternalUrl;
@@ -43,6 +71,16 @@ it('rejects private IPv4 addresses', function (string $url) {
'192.168.x range' => 'http://192.168.1.1',
]);
it('links private target errors to the outbound endpoint settings', function () {
$validator = Validator::make(
['url' => 'http://192.168.1.23'],
['url' => new SafeExternalUrl],
);
expect($validator->errors()->first('url'))
->toContain(route('settings.advanced').'#endpoint-section');
});
it('rejects cloud metadata IP', function () {
$rule = new SafeExternalUrl;
+13
View File
@@ -50,6 +50,19 @@ it('rejects link-local range', function () {
expect($validator->fails())->toBeTrue('Expected rejection: link-local IP');
});
it('rejects link-local targets even when allowlisted', function () {
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], [
'webhook_allowed_internal_hosts' => ['169.254.0.0/16'],
]));
$validator = Validator::make(
['url' => 'http://169.254.169.254/latest/meta-data'],
['url' => new SafeWebhookUrl],
);
expect($validator->fails())->toBeTrue();
});
it('rejects hostnames that resolve to blocked addresses', function (string $url, array $resolvedIps) {
$rule = new SafeWebhookUrl(fn (string $host): array => $resolvedIps);