Created
October 1, 2026 12:21
-
-
Save joseconti/557af26a260e0c85e875d78e69bb84cb to your computer and use it in GitHub Desktop.
Stop SPam/Splog WooCOmmerce Checkout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * WooCommerce Store API checkout protection. | |
| * | |
| * Requires a signed first-party cookie before allowing a Store API checkout. | |
| * This helps reject bots posting directly to /wc/store/v1/checkout without | |
| * affecting normal WooCommerce customer registration. | |
| */ | |
| defined( 'ABSPATH' ) || exit; | |
| /** | |
| * Cookie name used by the checkout protection. | |
| */ | |
| const JC_CHECKOUT_COOKIE = 'jc_checkout_protection'; | |
| /** | |
| * Minimum age, in seconds, before the cookie is accepted. | |
| * | |
| * A very small delay rejects many direct automated requests without making | |
| * legitimate customers wait during checkout. | |
| */ | |
| const JC_CHECKOUT_MIN_AGE = 2; | |
| /** | |
| * Maximum lifetime of the protection cookie. | |
| */ | |
| const JC_CHECKOUT_MAX_AGE = DAY_IN_SECONDS; | |
| /** | |
| * Creates the signed protection value. | |
| * | |
| * @param int $timestamp Timestamp when the token was generated. | |
| * @param string $random Random token component. | |
| * @return string | |
| */ | |
| function jc_checkout_create_token( $timestamp, $random ) { | |
| $payload = $timestamp . '|' . $random; | |
| $signature = hash_hmac( | |
| 'sha256', | |
| $payload, | |
| wp_salt( 'auth' ) | |
| ); | |
| return $payload . '|' . $signature; | |
| } | |
| /** | |
| * Sets a first-party protection cookie for frontend visitors. | |
| * | |
| * @return void | |
| */ | |
| function jc_checkout_set_protection_cookie() { | |
| if ( | |
| is_admin() || | |
| wp_doing_ajax() || | |
| wp_doing_cron() || | |
| headers_sent() | |
| ) { | |
| return; | |
| } | |
| if ( isset( $_COOKIE[ JC_CHECKOUT_COOKIE ] ) ) { | |
| return; | |
| } | |
| $timestamp = time(); | |
| $random = wp_generate_password( 32, false, false ); | |
| $value = jc_checkout_create_token( $timestamp, $random ); | |
| setcookie( | |
| JC_CHECKOUT_COOKIE, | |
| $value, | |
| array( | |
| 'expires' => $timestamp + JC_CHECKOUT_MAX_AGE, | |
| 'path' => COOKIEPATH ? COOKIEPATH : '/', | |
| 'domain' => COOKIE_DOMAIN, | |
| 'secure' => is_ssl(), | |
| 'httponly' => true, | |
| 'samesite' => 'Lax', | |
| ) | |
| ); | |
| /* | |
| * Make the cookie available during the current PHP request too. | |
| */ | |
| $_COOKIE[ JC_CHECKOUT_COOKIE ] = $value; | |
| } | |
| add_action( 'template_redirect', 'jc_checkout_set_protection_cookie', 1 ); | |
| /** | |
| * Validates the checkout protection cookie. | |
| * | |
| * @return bool | |
| */ | |
| function jc_checkout_has_valid_protection_cookie() { | |
| if ( ! isset( $_COOKIE[ JC_CHECKOUT_COOKIE ] ) ) { | |
| return false; | |
| } | |
| $cookie = sanitize_text_field( | |
| wp_unslash( $_COOKIE[ JC_CHECKOUT_COOKIE ] ) | |
| ); | |
| $parts = explode( '|', $cookie ); | |
| if ( 3 !== count( $parts ) ) { | |
| return false; | |
| } | |
| list( $timestamp, $random, $signature ) = $parts; | |
| if ( ! ctype_digit( $timestamp ) ) { | |
| return false; | |
| } | |
| $timestamp = (int) $timestamp; | |
| $age = time() - $timestamp; | |
| if ( | |
| JC_CHECKOUT_MIN_AGE > $age || | |
| JC_CHECKOUT_MAX_AGE < $age | |
| ) { | |
| return false; | |
| } | |
| $expected = hash_hmac( | |
| 'sha256', | |
| $timestamp . '|' . $random, | |
| wp_salt( 'auth' ) | |
| ); | |
| return hash_equals( $expected, $signature ); | |
| } | |
| /** | |
| * Blocks direct Store API checkout requests without a valid browser token. | |
| * | |
| * @param mixed $result REST pre-dispatch result. | |
| * @param WP_REST_Server $server REST server instance. | |
| * @param WP_REST_Request $request Current REST request. | |
| * @return mixed|WP_Error | |
| */ | |
| function jc_checkout_protect_store_api( $result, $server, $request ) { | |
| if ( | |
| 'POST' !== $request->get_method() || | |
| '/wc/store/v1/checkout' !== $request->get_route() | |
| ) { | |
| return $result; | |
| } | |
| if ( jc_checkout_has_valid_protection_cookie() ) { | |
| return $result; | |
| } | |
| /* | |
| * Log blocked attempts without storing checkout/customer data. | |
| */ | |
| if ( function_exists( 'wc_get_logger' ) ) { | |
| wc_get_logger()->warning( | |
| 'Blocked Store API checkout request without a valid browser token.', | |
| array( | |
| 'source' => 'jc-checkout-protection', | |
| ) | |
| ); | |
| } | |
| return new WP_Error( | |
| 'jc_checkout_protection_failed', | |
| __( | |
| 'Unable to process the checkout request. Please reload the page and try again.', | |
| 'jc-checkout-protection' | |
| ), | |
| array( | |
| 'status' => 403, | |
| ) | |
| ); | |
| } | |
| add_filter( | |
| 'rest_pre_dispatch', | |
| 'jc_checkout_protect_store_api', | |
| 10, | |
| 3 | |
| ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment