fix(paypal_v2): Webhook-Signatur mit rohem Body pruefen, Ablehnungsgrund protokollieren
This commit is contained in:
@@ -46,10 +46,11 @@ final class Client
|
||||
* @param list<string> $extraHeaders
|
||||
* @return array{0:int,1:array<mixed>}
|
||||
*/
|
||||
public function request(string $method, string $path, ?array $payload = null, array $extraHeaders = []): array
|
||||
public function request(string $method, string $path, array|string|null $payload = null, array $extraHeaders = []): array
|
||||
{
|
||||
$headers = \array_merge(['Content-Type: application/json', 'Authorization: Bearer ' . $this->accessToken()], $extraHeaders);
|
||||
$bodyStr = $payload === null ? null : (string) \json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
// string = fertiges JSON (z.B. mit dem rohen Webhook-Body eingebettet), wird unveraendert gesendet
|
||||
$bodyStr = $payload === null ? null : (\is_string($payload) ? $payload : (string) \json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
[$status, $body] = $this->raw($method, $path, $bodyStr, $headers);
|
||||
$decoded = $body === '' ? [] : \json_decode($body, true);
|
||||
return [$status, \is_array($decoded) ? $decoded : ['raw' => $body]];
|
||||
|
||||
@@ -212,8 +212,11 @@ final class Provider extends AbstractProvider
|
||||
}
|
||||
$type = (string) ($event['event_type'] ?? '');
|
||||
|
||||
if (!$this->verifyWebhook($headers, $event)) {
|
||||
Log::write($this->module(), 'webhook', null, null, (string) ($event['id'] ?? ''), 'rejected', null, ['event_type' => $type], 'Signatur ungueltig');
|
||||
$verify = $this->verifyWebhook($headers, $rawBody);
|
||||
if ($verify !== 'SUCCESS') {
|
||||
Log::write($this->module(), 'webhook', null, null, (string) ($event['id'] ?? ''), 'rejected', null,
|
||||
['event_type' => $type, 'header_keys' => \array_keys(\array_filter($headers, static fn($k) => \str_starts_with($k, 'paypal-'), ARRAY_FILTER_USE_KEY))],
|
||||
'Signatur ungueltig: ' . $verify);
|
||||
return 400;
|
||||
}
|
||||
|
||||
@@ -251,30 +254,43 @@ final class Provider extends AbstractProvider
|
||||
return 200;
|
||||
}
|
||||
|
||||
/** @param array<string,string> $headers @param array<mixed> $event */
|
||||
private function verifyWebhook(array $headers, array $event): bool
|
||||
/**
|
||||
* Signaturpruefung gegen PayPal. Der rohe Body wird UNVERAENDERT als webhook_event
|
||||
* eingebettet -- PayPal hat die Signatur ueber genau diesen Text gebildet; ein
|
||||
* json_decode/encode in PHP aendert Escaping und Reihenfolge und laesst die
|
||||
* Pruefung scheitern.
|
||||
*
|
||||
* @param array<string,string> $headers
|
||||
* @return string 'SUCCESS' oder Grund der Ablehnung
|
||||
*/
|
||||
private function verifyWebhook(array $headers, string $rawBody): string
|
||||
{
|
||||
$webhookId = Secrets::get($this->db, $this->module(), 'webhook_id') ?? '';
|
||||
if ($webhookId === '') {
|
||||
\error_log('PayPal v2 Webhook: keine webhook_id hinterlegt -- Event abgelehnt.');
|
||||
return false;
|
||||
return 'keine webhook_id hinterlegt';
|
||||
}
|
||||
$payload = [
|
||||
$fields = [
|
||||
'transmission_id' => $headers['paypal-transmission-id'] ?? '',
|
||||
'transmission_time' => $headers['paypal-transmission-time'] ?? '',
|
||||
'cert_url' => $headers['paypal-cert-url'] ?? '',
|
||||
'auth_algo' => $headers['paypal-auth-algo'] ?? '',
|
||||
'transmission_sig' => $headers['paypal-transmission-sig'] ?? '',
|
||||
'webhook_id' => $webhookId,
|
||||
'webhook_event' => $event,
|
||||
];
|
||||
foreach (['transmission_id', 'transmission_time', 'cert_url', 'auth_algo', 'transmission_sig'] as $k) {
|
||||
if ($payload[$k] === '') {
|
||||
return false;
|
||||
foreach ($fields as $k => $v) {
|
||||
if ($v === '') {
|
||||
return 'Header fehlt: ' . $k;
|
||||
}
|
||||
}
|
||||
[$status, $body] = $this->client()->request('POST', '/v1/notifications/verify-webhook-signature', $payload);
|
||||
return $status === 200 && (($body['verification_status'] ?? '') === 'SUCCESS');
|
||||
$json = \json_encode($fields, JSON_UNESCAPED_SLASHES);
|
||||
$json = \substr($json, 0, -1) . ',"webhook_event":' . \trim($rawBody) . '}';
|
||||
|
||||
[$status, $body] = $this->client()->request('POST', '/v1/notifications/verify-webhook-signature', $json);
|
||||
$vs = (string) ($body['verification_status'] ?? '');
|
||||
if ($status === 200 && $vs === 'SUCCESS') {
|
||||
return 'SUCCESS';
|
||||
}
|
||||
return 'HTTP ' . $status . ' ' . ($vs !== '' ? $vs : \json_encode($body));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- reconcile
|
||||
|
||||
Reference in New Issue
Block a user