fix(webhooks): add validation to block unsafe webhook URLs
Prevent server-side request forgery (SSRF) attacks by validating webhook URLs before sending requests. Blocks loopback addresses, cloud metadata endpoints, and localhost URLs. - Add SafeWebhookUrl rule validation in SendWebhookJob.handle() - Log warning when unsafe URLs are rejected - Add comprehensive unit tests covering valid and invalid URL scenarios
This commit is contained in:
parent
564cd8368b
commit
0b8c75f8ed
2 changed files with 93 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
77
tests/Unit/SendWebhookJobTest.php
Normal file
77
tests/Unit/SendWebhookJobTest.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
use App\Jobs\SendWebhookJob;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
it('sends webhook to valid URLs', function () {
|
||||
Http::fake(['*' => 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();
|
||||
});
|
||||
Loading…
Reference in a new issue