From 12ee7f318c39f4e6e8f59a2fa24378bdfd0737df Mon Sep 17 00:00:00 2001 From: David Brownman Date: Thu, 19 Mar 2026 16:50:28 -0700 Subject: [PATCH 1/2] Add errors when parsing the wrong kind of webhooks payload --- lib/V2/Core/EventNotification.php | 8 +++++++- lib/Webhook.php | 8 +++++++- tests/Stripe/BaseStripeClientTest.php | 26 ++++++++++++++++++++++++-- tests/Stripe/EventTest.php | 6 +++--- tests/Stripe/WebhookTest.php | 17 +++++++++++++++++ 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/lib/V2/Core/EventNotification.php b/lib/V2/Core/EventNotification.php index 35bc921aa..e285f9d3c 100644 --- a/lib/V2/Core/EventNotification.php +++ b/lib/V2/Core/EventNotification.php @@ -65,7 +65,7 @@ public function __construct($json, $client) /** * Helper for constructing an Event Notification. Doesn't perform signature validation, so you - * should use \Stripe\BaseStripeClient#parseEventNotification instead for + * should use \Stripe\BaseStripeClient::parseEventNotification instead for * initial handling. This is useful in unit tests and working with EventNotifications that you've * already validated the authenticity of. * @@ -78,6 +78,12 @@ public static function fromJson($jsonStr, $client) { $json = json_decode($jsonStr, true); + if (isset($json['object']) && 'event' === $json['object']) { + throw new \Stripe\Exception\UnexpectedValueException( + 'You passed a webhook payload to StripeClient::parseEventNotification, which expects an event notification. Use Webhook::constructEvent instead.' + ); + } + $class = UnknownEventNotification::class; $eventNotificationTypes = EventNotificationTypes::v2EventMapping; if (\array_key_exists($json['type'], $eventNotificationTypes)) { diff --git a/lib/Webhook.php b/lib/Webhook.php index 6f4e9c3cc..1a7dbc2cb 100644 --- a/lib/Webhook.php +++ b/lib/Webhook.php @@ -32,11 +32,17 @@ public static function constructEvent($payload, $sigHeader, $secret, $tolerance $jsonError = \json_last_error(); if (null === $data && \JSON_ERROR_NONE !== $jsonError) { $msg = "Invalid payload: {$payload} " - . "(json_last_error() was {$jsonError})"; + . "(json_last_error() was {$jsonError})"; throw new Exception\UnexpectedValueException($msg); } + if (isset($data['object']) && 'v2.core.event' === $data['object']) { + throw new Exception\UnexpectedValueException( + 'You passed an event notification to Webhook::constructEvent, which expects a webhook payload. Use StripeClient::parseEventNotification instead.' + ); + } + return Event::constructFrom($data); } } diff --git a/tests/Stripe/BaseStripeClientTest.php b/tests/Stripe/BaseStripeClientTest.php index 36dca9f7b..ceceaafb9 100644 --- a/tests/Stripe/BaseStripeClientTest.php +++ b/tests/Stripe/BaseStripeClientTest.php @@ -837,7 +837,7 @@ public function testParseEventNotification() { $jsonEvent = [ 'id' => 'evt_234', - 'object' => 'event', + 'object' => 'v2.core.event', 'type' => 'v1.billing.meter.error_report_triggered', 'created' => '2022-02-15T00:27:45.330Z', 'context' => 'acct_123', @@ -872,7 +872,7 @@ public function testParseUnknownEventNotification() { $jsonEvent = [ 'id' => 'evt_234', - 'object' => 'event', + 'object' => 'v2.core.event', 'type' => 'imaginary', 'livemode' => true, 'created' => '2022-02-15T00:27:45.330Z', @@ -896,6 +896,28 @@ public function testParseUnknownEventNotification() self::assertNull($event->fetchRelatedObject()); } + public function testParseEventNotificationRejectsV1Payload() + { + $jsonEvent = [ + 'id' => 'evt_234', + 'object' => 'event', + 'type' => 'charge.succeeded', + 'data' => ['object' => ['id' => 'ch_123', 'object' => 'charge']], + ]; + + $eventData = json_encode($jsonEvent); + $client = new BaseStripeClient(['api_key' => 'sk_test_client', 'api_base' => MOCK_URL, 'stripe_account' => 'acc_123']); + + $sigHeader = WebhookTest::generateHeader(['payload' => $eventData]); + + try { + $client->parseEventNotification($eventData, $sigHeader, WebhookTest::SECRET); + self::fail('Expected UnexpectedValueException was not thrown'); + } catch (Exception\UnexpectedValueException $e) { + self::assertStringContainsString('Webhook::constructEvent', $e->getMessage()); + } + } + public function testV2OverridesPreviewVersionIfPassedInRawRequestOptions() { $this->curlClientStub->method('executeRequestWithRetries') diff --git a/tests/Stripe/EventTest.php b/tests/Stripe/EventTest.php index 7be881625..73a0fd5a6 100644 --- a/tests/Stripe/EventTest.php +++ b/tests/Stripe/EventTest.php @@ -187,7 +187,7 @@ public function testJsonDecodeEventNotificationObject() { $eventData = json_encode([ 'id' => 'evt_234', - 'object' => 'event', + 'object' => 'v2.core.event', 'type' => 'v1.billing.meter.error_report_triggered', 'created' => '2022-02-15T00:27:45.330Z', 'related_object' => [ @@ -226,7 +226,7 @@ public function testJsonDecodeEventNotificationObjectWithNoRelatedObject() { $eventData = json_encode([ 'id' => 'evt_234', - 'object' => 'event', + 'object' => 'v2.core.event', 'type' => 'v1.billing.meter.no_meter_found', 'created' => '2022-02-15T00:27:45.330Z', ]); @@ -244,7 +244,7 @@ public function testJsonDecodeEventNotificationObjectWithNoReasonObject() { $eventData = json_encode([ 'id' => 'evt_234', - 'object' => 'event', + 'object' => 'v2.core.event', 'type' => 'imaginary', 'created' => '2022-02-15T00:27:45.330Z', ]); diff --git a/tests/Stripe/WebhookTest.php b/tests/Stripe/WebhookTest.php index b8bd08669..f44691bae 100644 --- a/tests/Stripe/WebhookTest.php +++ b/tests/Stripe/WebhookTest.php @@ -121,4 +121,21 @@ public function testTimestampOffButNoTolerance() $sigHeader = $this->generateHeader(['timestamp' => 12345]); self::assertTrue(WebhookSignature::verifyHeader(self::EVENT_PAYLOAD, $sigHeader, self::SECRET)); } + + public function testConstructEventRejectsV2Payload() + { + $payload = json_encode([ + 'id' => 'evt_test_webhook', + 'object' => 'v2.core.event', + 'type' => 'v1.billing.meter.error_report_triggered', + ]); + $sigHeader = $this->generateHeader(['payload' => $payload]); + + try { + Webhook::constructEvent($payload, $sigHeader, self::SECRET); + self::fail('Expected UnexpectedValueException was not thrown'); + } catch (Exception\UnexpectedValueException $e) { + self::assertStringContainsString('StripeClient::parseEventNotification', $e->getMessage()); + } + } } From ec6b4c13fd1081046cf74faf7cd0323fcbb7ff13 Mon Sep 17 00:00:00 2001 From: David Brownman Date: Thu, 19 Mar 2026 16:54:05 -0700 Subject: [PATCH 2/2] Fix tests --- tests/Stripe/BaseStripeClientTest.php | 2 +- tests/Stripe/WebhookTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Stripe/BaseStripeClientTest.php b/tests/Stripe/BaseStripeClientTest.php index ceceaafb9..660178d98 100644 --- a/tests/Stripe/BaseStripeClientTest.php +++ b/tests/Stripe/BaseStripeClientTest.php @@ -914,7 +914,7 @@ public function testParseEventNotificationRejectsV1Payload() $client->parseEventNotification($eventData, $sigHeader, WebhookTest::SECRET); self::fail('Expected UnexpectedValueException was not thrown'); } catch (Exception\UnexpectedValueException $e) { - self::assertStringContainsString('Webhook::constructEvent', $e->getMessage()); + self::compatAssertStringContainsString('Webhook::constructEvent', $e->getMessage()); } } diff --git a/tests/Stripe/WebhookTest.php b/tests/Stripe/WebhookTest.php index f44691bae..49e7fb4a8 100644 --- a/tests/Stripe/WebhookTest.php +++ b/tests/Stripe/WebhookTest.php @@ -135,7 +135,7 @@ public function testConstructEventRejectsV2Payload() Webhook::constructEvent($payload, $sigHeader, self::SECRET); self::fail('Expected UnexpectedValueException was not thrown'); } catch (Exception\UnexpectedValueException $e) { - self::assertStringContainsString('StripeClient::parseEventNotification', $e->getMessage()); + self::compatAssertStringContainsString('StripeClient::parseEventNotification', $e->getMessage()); } } }