PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
← All changes | includes/square/gateway/class-charitable-square-webhook-processor.php +157 -1 1.8.11 → 1.8.13 View file →
@@ -962,9 +962,19 @@
962 962 error_log( 'Square Webhook - Transaction ID: ' . $transaction_id );
963 963 // phpcs:enable
964 964 }
965 965
966 - $refund_amount = $charge['refund']['amount_money']['amount'];
966 + $refund_amount = $charge['refund']['amount_money']['amount'];
967 + $refund_currency = $charge['refund']['amount_money']['currency'];
968 +
969 + // Square sends amounts in the smallest currency unit (cents for
970 + // USD, yen for JPY, etc.). process_refund() expects the site's
971 + // display unit, so divide by 100 unless this is a zero-decimal
972 + // currency.
973 + if ( ! Charitable_Square_Gateway_Processor::is_zero_decimal_currency( $refund_currency ) ) {
974 + $refund_amount = $refund_amount / 100;
975 + }
976 +
967 977 if ( charitable_is_debug( 'square' ) ) {
968 978 // phpcs:disable
969 979 error_log( 'Square Webhook - Refund Amount: ' . $refund_amount );
970 980 // phpcs:enable
@@ -1345,8 +1355,154 @@
1345 1355 error_log( 'Raw webhook body: ' . $body );
1346 1356 error_log( 'Request method: ' . $_SERVER['REQUEST_METHOD'] );
1347 1357 error_log( 'Content type: ' . ( isset( $_SERVER['CONTENT_TYPE'] ) ? $_SERVER['CONTENT_TYPE'] : 'not set' ) );
1348 1358 // phpcs:enable
1359 + }
1360 +
1361 + /*
1362 + * Verify Square's webhook signature before trusting the payload.
1363 + *
1364 + * Square signs every webhook by computing an HMAC-SHA256 over
1365 + * ( notification_url . request_body ) using the subscription's
1366 + * signature key, and sends the base64 result in the
1367 + * x-square-hmacsha256-signature header. We recompute it with the
1368 + * stored signing secret and reject anything that does not match.
1369 + * Without this an unauthenticated caller could POST a forged event
1370 + * to the public charitable-listener=square_core endpoint and tamper
1371 + * with donation, refund, and subscription records.
1372 + *
1373 + * @since 1.8.11.3
1374 + */
1375 + $signature_header = isset( $_SERVER['HTTP_X_SQUARE_HMACSHA256_SIGNATURE'] ) ? wp_unslash( $_SERVER['HTTP_X_SQUARE_HMACSHA256_SIGNATURE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- A cryptographic signature is compared byte-for-byte and must not be altered by sanitization.
1376 + $mode = Charitable_Square_Helpers::get_mode();
1377 +
1378 + /*
1379 + * The signing secret is stored per-mode by the webhooks manager.
1380 + * Lite saves it under the `gateways_square` group; read the
1381 + * `gateways_square_core` group first for forward-compatibility and
1382 + * fall back to `gateways_square` where it actually lives.
1383 + */
1384 + $core_settings = charitable_get_option( 'gateways_square_core' );
1385 + $legacy_settings = charitable_get_option( 'gateways_square' );
1386 + $signature_key = '';
1387 + if ( is_array( $core_settings ) && ! empty( $core_settings[ $mode ]['webhooks-secret'] ) ) {
1388 + $signature_key = $core_settings[ $mode ]['webhooks-secret'];
1389 + } elseif ( is_array( $legacy_settings ) && ! empty( $legacy_settings[ $mode ]['webhooks-secret'] ) ) {
1390 + $signature_key = $legacy_settings[ $mode ]['webhooks-secret'];
1391 + }
1392 +
1393 + /*
1394 + * Verify against the exact URL registered with Square
1395 + * ( Charitable_Square_WebhooksManager registers with
1396 + * get_webhook_url() ), so sites using the CHARITABLE_SQUARE_WHURL
1397 + * override still validate. Fall back to the REST URL if needed.
1398 + */
1399 + $notification_url = Charitable_Square_Helpers::get_webhook_url();
1400 + if ( empty( $notification_url ) ) {
1401 + $notification_url = Charitable_Square_Helpers::get_webhook_url_for_rest();
1402 + }
1403 +
1404 + if ( charitable_is_debug( 'square' ) ) {
1405 + // phpcs:disable
1406 + error_log( 'Square webhook signature header: ' . ( ! empty( $signature_header ) ? 'present' : 'missing' ) );
1407 + error_log( 'Square webhook signing secret: ' . ( ! empty( $signature_key ) ? 'stored' : 'not stored' ) );
1408 + // phpcs:enable
1409 + }
1410 +
1411 + if ( ! empty( $signature_key ) && ! empty( $signature_header ) ) {
1412 +
1413 + /*
1414 + * Resolve the Square SDK webhook helper. Lite bundles the SDK
1415 + * under the unprefixed \Square namespace; also accept a
1416 + * prefixed (scoped) build so a future packaging change cannot
1417 + * silently disable verification.
1418 + */
1419 + $helper_class = '';
1420 + if ( class_exists( '\Square\Utils\WebhooksHelper' ) ) {
1421 + $helper_class = '\Square\Utils\WebhooksHelper';
1422 + } elseif ( class_exists( '\CharitableSquareSDK\Utils\WebhooksHelper' ) ) {
1423 + $helper_class = '\CharitableSquareSDK\Utils\WebhooksHelper';
1424 + }
1425 +
1426 + if ( '' === $helper_class ) {
1427 + // Fail closed: a signed request we cannot verify is treated
1428 + // as untrusted. Partial verification is worse than none.
1429 + error_log( 'Square webhook REJECTED: WebhooksHelper class unavailable, cannot verify signature.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1430 + return false;
1431 + }
1432 +
1433 + try {
1434 + $is_valid = $helper_class::verifySignature( $body, $signature_header, $signature_key, $notification_url );
1435 + } catch ( \Throwable $e ) {
1436 + // Fail closed on any error/exception from the SDK: a signed
1437 + // request we could not verify is treated as untrusted.
1438 + if ( charitable_is_debug( 'square' ) ) {
1439 + error_log( 'Square webhook signature verification error: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1440 + }
1441 + return false;
1442 + }
1443 +
1444 + if ( ! $is_valid ) {
1445 + if ( charitable_is_debug( 'square' ) ) {
1446 + error_log( 'Square webhook REJECTED: signature verification failed.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1447 + }
1448 + return false;
1449 + }
1450 +
1451 + if ( charitable_is_debug( 'square' ) ) {
1452 + error_log( 'Square webhook signature verified OK.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1453 + }
1454 + } elseif ( ! empty( $signature_key ) && empty( $signature_header ) ) {
1455 + // A signing secret is configured but the request is unsigned: reject.
1456 + if ( charitable_is_debug( 'square' ) ) {
1457 + error_log( 'Square webhook REJECTED: signing secret configured but request has no signature header.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1458 + }
1459 + return false;
1460 + }
1461 +
1462 + /*
1463 + * No signing secret stored. Historically the processor fell through
1464 + * here to a merchant_id presence check and ACCEPTED the (unsigned)
1465 + * event, which let an unauthenticated caller forge Square webhooks on
1466 + * any site that had not yet stored a signing secret. Fail closed: the
1467 + * event is refused unless enforcement is explicitly disabled.
1468 + *
1469 + * Sites connected through the Square settings screen store a signing
1470 + * secret during onboarding, so they never reach this branch and are
1471 + * unaffected. For the rare site that has webhooks enabled but no
1472 + * stored secret, refused events stop donations/refunds from
1473 + * reconciling, which the hourly Charitable_Square_WebhooksHealthCheck
1474 + * detects and surfaces as an admin notice prompting a reconnect (which
1475 + * re-registers the subscription and stores a fresh signing secret,
1476 + * restoring verification).
1477 + *
1478 + * @since 1.8.12
1479 + */
1480 + if ( empty( $signature_key ) ) {
1481 +
1482 + /**
1483 + * Filter whether to enforce Square webhook signature verification
1484 + * when no signing secret is stored.
1485 + *
1486 + * Defaults to true (fail closed). Returning false restores the
1487 + * legacy behaviour of accepting unsigned events and re-opens the
1488 + * forgery vector, so it should only be used as a temporary escape
1489 + * hatch while reconnecting Square.
1490 + *
1491 + * @since 1.8.12
1492 + *
1493 + * @param bool $enforce Whether to reject unsigned events. Default true.
1494 + */
1495 + $enforce_signature = (bool) apply_filters( 'charitable_square_webhook_enforce_signature', true );
1496 +
1497 + if ( $enforce_signature ) {
1498 + error_log( 'Square webhook REJECTED: no signing secret stored. Reconnect Square (Settings > Payment > Square) to register webhooks and enable signature verification. To temporarily accept unsigned events, use the charitable_square_webhook_enforce_signature filter.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1499 + return false;
1500 + }
1501 +
1502 + if ( charitable_is_debug( 'square' ) ) {
1503 + error_log( 'Square webhook WARNING: no signing secret stored and signature enforcement disabled via the charitable_square_webhook_enforce_signature filter - accepting unsigned event.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1504 + }
1349 1505 }
1350 1506
1351 1507 $event = json_decode( $body, true );
1352 1508