diff --git a/app/Jobs/SendWebhookJob.php b/app/Jobs/SendWebhookJob.php index 607fda3fe..9d2a94606 100644 --- a/app/Jobs/SendWebhookJob.php +++ b/app/Jobs/SendWebhookJob.php @@ -9,6 +9,8 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Validator; class SendWebhookJob implements ShouldBeEncrypted, ShouldQueue { @@ -40,6 +42,20 @@ public function __construct( */ public function handle(): void { + $validator = Validator::make( + ['webhook_url' => $this->webhookUrl], + ['webhook_url' => ['required', 'url', new \App\Rules\SafeWebhookUrl]] + ); + + if ($validator->fails()) { + Log::warning('SendWebhookJob: blocked unsafe webhook URL', [ + 'url' => $this->webhookUrl, + 'errors' => $validator->errors()->all(), + ]); + + return; + } + if (isDev()) { ray('Sending webhook notification', [ 'url' => $this->webhookUrl, diff --git a/tests/Unit/SendWebhookJobTest.php b/tests/Unit/SendWebhookJobTest.php new file mode 100644 index 000000000..688cd3bf2 --- /dev/null +++ b/tests/Unit/SendWebhookJobTest.php @@ -0,0 +1,77 @@ + Http::response('ok', 200)]); + + $job = new SendWebhookJob( + payload: ['event' => 'test'], + webhookUrl: 'https://example.com/webhook' + ); + + $job->handle(); + + Http::assertSent(function ($request) { + return $request->url() === 'https://example.com/webhook'; + }); +}); + +it('blocks webhook to loopback address', function () { + Http::fake(); + Log::shouldReceive('warning') + ->once() + ->withArgs(function ($message) { + return str_contains($message, 'blocked unsafe webhook URL'); + }); + + $job = new SendWebhookJob( + payload: ['event' => 'test'], + webhookUrl: 'http://127.0.0.1/admin' + ); + + $job->handle(); + + Http::assertNothingSent(); +}); + +it('blocks webhook to cloud metadata endpoint', function () { + Http::fake(); + Log::shouldReceive('warning') + ->once() + ->withArgs(function ($message) { + return str_contains($message, 'blocked unsafe webhook URL'); + }); + + $job = new SendWebhookJob( + payload: ['event' => 'test'], + webhookUrl: 'http://169.254.169.254/latest/meta-data/' + ); + + $job->handle(); + + Http::assertNothingSent(); +}); + +it('blocks webhook to localhost', function () { + Http::fake(); + Log::shouldReceive('warning') + ->once() + ->withArgs(function ($message) { + return str_contains($message, 'blocked unsafe webhook URL'); + }); + + $job = new SendWebhookJob( + payload: ['event' => 'test'], + webhookUrl: 'http://localhost/internal-api' + ); + + $job->handle(); + + Http::assertNothingSent(); +});