diff --git a/app/Domains/Webhook/Jobs/DeliverWebhookJob.php b/app/Domains/Webhook/Jobs/DeliverWebhookJob.php index 6377a84..000a48c 100644 --- a/app/Domains/Webhook/Jobs/DeliverWebhookJob.php +++ b/app/Domains/Webhook/Jobs/DeliverWebhookJob.php @@ -38,18 +38,21 @@ class DeliverWebhookJob implements ShouldQueue return; } + $decodedPayload = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + $response = Http::timeout(10) ->withHeaders([ 'Content-Type' => 'application/json', 'X-Nimuli-Signature' => $signature, 'X-Nimuli-Event' => $this->event, ]) - ->post($this->webhook->url, json_decode($body, true, 512, JSON_THROW_ON_ERROR)); + ->withBody($body, 'application/json') + ->post($this->webhook->url); WebhookDelivery::create([ 'webhook_id' => $this->webhook->id, 'event' => $this->event, - 'payload' => json_decode($body, true, 512, JSON_THROW_ON_ERROR), + 'payload' => $decodedPayload, 'status' => $response->successful() ? 'success' : 'failed', 'response_code' => $response->status(), 'attempts' => max(1, $this->attempts()), @@ -62,12 +65,42 @@ class DeliverWebhookJob implements ShouldQueue return true; } - $ip = gethostbyname($host); + // Strip IPv6 brackets: [::1] → ::1 + $host = ltrim(rtrim($host, ']'), '['); - return filter_var( - $ip, - FILTER_VALIDATE_IP, - FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE - ) === false; + // If it's already an IP, check it directly (bypasses DNS resolution) + if (filter_var($host, FILTER_VALIDATE_IP)) { + return ! filter_var( + $host, + FILTER_VALIDATE_IP, + FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE + ) || $this->isPrivateIPv6($host); + } + + // Resolve hostname to IPv4 + $ip = gethostbyname($host); + if ($ip === $host) { + // Could not resolve or is an IPv6 hostname that gethostbyname can't handle — block + return true; + } + + return ! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); + } + + private function isPrivateIPv6(string $ip): bool + { + if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return false; + } + + // Block loopback, link-local, unique local, unspecified + $privateRanges = ['::1', 'fe80::', 'fc00::', 'fd00::', '::']; + foreach ($privateRanges as $range) { + if (str_starts_with($ip, $range)) { + return true; + } + } + + return false; } } diff --git a/app/Domains/Webhook/Models/WebhookDelivery.php b/app/Domains/Webhook/Models/WebhookDelivery.php index 050cf8e..bef02d3 100644 --- a/app/Domains/Webhook/Models/WebhookDelivery.php +++ b/app/Domains/Webhook/Models/WebhookDelivery.php @@ -10,7 +10,6 @@ class WebhookDelivery extends Model protected $casts = [ 'payload' => 'array', - 'delivered_at' => 'datetime', ]; public function webhook(): \Illuminate\Database\Eloquent\Relations\BelongsTo