2a34ee880a
- /api/dashboard/summary, /api/managers, /api/lead-statuses: были без auth (tenant_id параметром) → auth:sanctum (+tenant); tenant_id из authed-user, не из параметра — закрывает кросс-tenant утечку KPI/списка пользователей - ManagerController: явный where(tenant_id) поверх RLS (BYPASSRLS-роли/тесты) - WebhookUrlGuard + webhooks/test: SSRF-блок private/reserved/loopback IP (cloud-metadata 169.254.169.254 и пр.); update()/delivery — follow-up - TDD: +EndpointAuthHardeningTest(5) +WebhookSsrfGuardTest(10); обновлены Dashboard/Lookups/LeadStatuses тесты под auth - регрессия tests/Feature 960/964 (2 фейла pre-existing: Vite-manifest env + RouteSupplierLeadJobBilling idempotency — оба фейлят и на чистом base) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
68 lines
2.4 KiB
PHP
68 lines
2.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\OutboundWebhookSubscription;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Support\WebhookUrlGuard;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = Tenant::factory()->create();
|
|
$this->user = User::factory()->for($this->tenant)->create();
|
|
$this->actingAs($this->user);
|
|
});
|
|
|
|
// --- unit: WebhookUrlGuard (IP-литералы, без DNS) ---
|
|
|
|
test('WebhookUrlGuard блокирует приватные/зарезервированные/loopback IP', function (string $url) {
|
|
expect(WebhookUrlGuard::blockReason($url))->not->toBeNull();
|
|
})->with([
|
|
'https://127.0.0.1/hook', // loopback
|
|
'https://10.0.0.1/hook', // private A
|
|
'https://172.16.0.1/hook', // private B
|
|
'https://192.168.1.1/hook', // private C
|
|
'https://169.254.169.254/hook', // link-local / cloud metadata
|
|
'https://[::1]/hook', // IPv6 loopback
|
|
]);
|
|
|
|
test('WebhookUrlGuard пропускает публичный IP', function () {
|
|
expect(WebhookUrlGuard::blockReason('https://93.184.216.34/hook'))->toBeNull();
|
|
});
|
|
|
|
test('WebhookUrlGuard отклоняет битый URL', function () {
|
|
expect(WebhookUrlGuard::blockReason('not-a-url'))->not->toBeNull();
|
|
});
|
|
|
|
// --- endpoint: webhooks/test не должен бить во внутреннюю сеть ---
|
|
|
|
test('POST webhooks/test блокирует приватный IP target_url (SSRF) и не шлёт запрос', function () {
|
|
Http::fake();
|
|
OutboundWebhookSubscription::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'user_id' => $this->user->id,
|
|
'target_url' => 'https://169.254.169.254/hook',
|
|
]);
|
|
|
|
$this->postJson('/api/webhooks/test')->assertStatus(422);
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
test('POST webhooks/test пропускает публичный target_url', function () {
|
|
Http::fake(['*' => Http::response(['ok' => true], 200)]);
|
|
OutboundWebhookSubscription::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'user_id' => $this->user->id,
|
|
'target_url' => 'https://93.184.216.34/hook',
|
|
]);
|
|
|
|
$this->postJson('/api/webhooks/test')
|
|
->assertOk()
|
|
->assertJsonPath('ok', true);
|
|
Http::assertSentCount(1);
|
|
});
|