feat(security): webhook DNS-rebind пиннинг + аддитивный HMAC supplier-webhook — edge/P2 go-live
WebhookUrlGuard::safeDeliveryIp один резолв + CURLOPT_RESOLVE пиннинг в test(); supplier-webhook принимает HMAC X-Webhook-Signature как альтернативу URL-секрету + secretless-маршрут. Аддитивно, backward-compat. 6 новых тестов GREEN; 5 падений webhook-сюиты pre-existing (Phase-3 B-regex + CsvWebhookRaceTest), подтверждено baseline без моих файлов. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -127,15 +127,16 @@ class WebhookSettingsController extends Controller
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
// SSRF-гард: target_url задаёт админ тенанта; блокируем адреса во
|
||||
// внутренней/зарезервированной сети (cloud-metadata 169.254.169.254,
|
||||
// loopback, RFC1918), которые https://-валидация на сохранении не ловит.
|
||||
$blockReason = WebhookUrlGuard::blockReason($sub->target_url);
|
||||
if ($blockReason !== null) {
|
||||
// SSRF-гард + DNS-rebind пиннинг: ОДИН резолв target_url даёт причину
|
||||
// блокировки И безопасный IP. Блокируем адреса во внутренней/зарезервированной
|
||||
// сети (cloud-metadata 169.254.169.254, loopback, RFC1918), которые
|
||||
// https://-валидация на сохранении не ловит.
|
||||
$delivery = WebhookUrlGuard::safeDeliveryIp($sub->target_url);
|
||||
if ($delivery['blockReason'] !== null) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'status' => null,
|
||||
'message' => $blockReason,
|
||||
'message' => $delivery['blockReason'],
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
@@ -145,9 +146,19 @@ class WebhookSettingsController extends Controller
|
||||
'message' => 'Тестовая доставка webhook от Лидерра.',
|
||||
];
|
||||
|
||||
// DNS-rebind пиннинг: подключаемся к УЖЕ проверенному IP, не давая
|
||||
// HTTP-клиенту резолвить хост повторно (TOCTOU). Host/SNI — исходный хост.
|
||||
$httpOptions = [];
|
||||
if ($delivery['ip'] !== null) {
|
||||
$host = trim((string) parse_url($sub->target_url, PHP_URL_HOST), '[]');
|
||||
$port = parse_url($sub->target_url, PHP_URL_PORT) ?? 443;
|
||||
$httpOptions['curl'] = [CURLOPT_RESOLVE => ["{$host}:{$port}:{$delivery['ip']}"]];
|
||||
}
|
||||
|
||||
// Unsigned connectivity-проверка (HMAC-подписанная доставка — отдельный эпик).
|
||||
try {
|
||||
$response = Http::timeout(10)
|
||||
$response = Http::withOptions($httpOptions)
|
||||
->timeout(10)
|
||||
->withHeaders(['X-Webhook-Event' => 'webhook.test'])
|
||||
->post($sub->target_url, $testPayload);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user