mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 18:18:29 +00:00
fix(webhooks): resolve hostnames using custom DNS servers
Keep webhook SSRF DNS checks active when general DNS validation is disabled, and include localhost loopback resolution in safe URL validation.
This commit is contained in:
1 parent
0bf97df9af
commit
3988ad6921
2 files changed
+80
No files matched your search
@@ -6,6 +6,8 @@ use App\Models\InstanceSettings;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use PurplePixie\PhpDns\DNSQuery;
|
||||
use PurplePixie\PhpDns\DNSTypes;
|
||||
use Throwable;
|
||||
|
||||
class SafeWebhookUrl implements ValidationRule
|
||||
@@ -208,6 +210,15 @@ class SafeWebhookUrl implements ValidationRule
|
||||
return array_values(array_filter(($this->resolver)($host), fn (string $ip): bool => filter_var($ip, FILTER_VALIDATE_IP) !== false));
|
||||
}
|
||||
|
||||
if ($host === 'localhost') {
|
||||
return ['127.0.0.1', '::1'];
|
||||
}
|
||||
|
||||
$customDnsServers = $this->customDnsServers();
|
||||
if ($customDnsServers !== []) {
|
||||
return $this->resolveHostWithCustomDnsServers($host, $customDnsServers);
|
||||
}
|
||||
|
||||
$records = @dns_get_record($host, DNS_A | DNS_AAAA);
|
||||
if ($records === false) {
|
||||
$records = [];
|
||||
@@ -234,6 +245,55 @@ class SafeWebhookUrl implements ValidationRule
|
||||
return array_values(array_unique($ips));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $dnsServers
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function resolveHostWithCustomDnsServers(string $host, array $dnsServers): array
|
||||
{
|
||||
$ips = [];
|
||||
|
||||
foreach ($dnsServers as $dnsServer) {
|
||||
foreach ([DNSTypes::NAME_A, DNSTypes::NAME_AAAA] as $type) {
|
||||
try {
|
||||
$query = new DNSQuery($dnsServer, 53, 5);
|
||||
$records = $query->query($host, $type);
|
||||
|
||||
if ($records === false || $query->hasError()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
if ($record->getType() === $type && filter_var($record->getData(), FILTER_VALIDATE_IP)) {
|
||||
$ips[] = $record->getData();
|
||||
}
|
||||
}
|
||||
} catch (Throwable) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($ips));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function customDnsServers(): array
|
||||
{
|
||||
$servers = $this->instanceSettings()?->custom_dns_servers ?? '';
|
||||
|
||||
if (! is_string($servers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(
|
||||
fn (string $server): string => trim($server),
|
||||
explode(',', $servers),
|
||||
), fn (string $server): bool => filter_var($server, FILTER_VALIDATE_IP) !== false));
|
||||
}
|
||||
|
||||
private function isAllowedIp(string $ip, string $host): bool
|
||||
{
|
||||
$embeddedIpv4 = $this->extractIpv4FromMappedIpv6($ip);
|
||||
|
||||
@@ -159,6 +159,26 @@ it('rejects unresolvable hostnames by default', function () {
|
||||
expect($validator->fails())->toBeTrue('Expected default rejection for unresolvable host');
|
||||
});
|
||||
|
||||
it('keeps webhook DNS resolution enabled when general DNS validation is disabled', function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], ['is_dns_validation_enabled' => false]));
|
||||
|
||||
$rule = new SafeWebhookUrl(fn (string $host): array => ['127.0.0.1']);
|
||||
|
||||
$validator = Validator::make(['url' => 'http://rebinding.example.test/webhook'], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue('Expected webhook SSRF DNS checks to remain enabled');
|
||||
});
|
||||
|
||||
it('reads configured custom DNS servers for webhook hostname resolution', function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], ['custom_dns_servers' => '1.1.1.1, invalid, 2606:4700:4700::1111']));
|
||||
|
||||
$method = new ReflectionMethod(SafeWebhookUrl::class, 'customDnsServers');
|
||||
$method->setAccessible(true);
|
||||
|
||||
expect($method->invoke(new SafeWebhookUrl))
|
||||
->toBe(['1.1.1.1', '2606:4700:4700::1111']);
|
||||
});
|
||||
|
||||
it('allows explicitly configured intranet webhook targets', function (string $url, array $resolvedIps, array $allowlist) {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], ['webhook_allowed_internal_hosts' => $allowlist]));
|
||||
|
||||
|
||||
Reference in new issue
Block a user