From 0d54ae6f1a7a54fe115887179e576391d898f38e Mon Sep 17 00:00:00 2001 From: Chris Portscheller Date: Sun, 19 Jul 2026 18:00:23 -0500 Subject: [PATCH] feat(woocommerce): honeytoken coupons (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the WooCommerce carding defense with a deterministic deception signal. - honeytoken_coupon(): a per-site fake promo code (WD + 8 hex, deterministic from a stored secret, optional daily rotation with grace). - render_coupon_bait(): plants the code on cart/checkout in an HTML comment + an offscreen aria-hidden node — where coupon-scraping bots look, invisible to humans and the accessibility tree. - catch_coupon(): hooks woocommerce_get_shop_coupon_data, so it fires for both classic checkout and the Store API / Blocks (both load coupons via WC_Coupon). Applying the canary (case-insensitive, incl. yesterday's during rotation) records a detection, optionally blocks the IP per block_action, and returns it as a non-existent coupon. Real coupons pass through untouched. - Existing velocity / card-testing detection is unchanged; this is additive. New WooCommerce settings toggle (on by default). Verified: code determinism, hidden bait markup, catch+record+block on the canary, case-insensitivity, rotation grace, log-mode (record no block), disabled passthrough, and real-coupon passthrough (16 checks). Closes #14. Part of #16. Co-authored-by: Claude --- admin/partials/settings-page.php | 11 +++ changelog.txt | 1 + includes/class-webdecoy-woocommerce.php | 101 ++++++++++++++++++++++++ webdecoy.php | 4 + 4 files changed, 117 insertions(+) diff --git a/admin/partials/settings-page.php b/admin/partials/settings-page.php index fe67bc1..04c6a5d 100644 --- a/admin/partials/settings-page.php +++ b/admin/partials/settings-page.php @@ -636,6 +636,17 @@ + + + + +

+ + diff --git a/changelog.txt b/changelog.txt index b289a61..4452094 100644 --- a/changelog.txt +++ b/changelog.txt @@ -9,6 +9,7 @@ * Added: Rule engine — deterministic rules evaluated before heuristic scoring; first DENY/THROTTLE wins, with dry-run (log without blocking). Parity with @webdecoy/node. * Added: Tripwires (deception layer) — deterministic, zero-false-positive blocking of hidden honeypot paths (scanner-bait like /.env, /.git/config, /wp-config.php). On by default. Custom exact paths, prefixes, and regex patterns; block or throttle; dry-run. New Settings → Tripwires tab. * Added: Honeytoken — automatically injects an invisible decoy link on front-end pages pointing at a secret per-site path; only link-following scrapers ever request it, and a hit is armed as a tripwire. On by default, with optional daily rotation. Never shown to real visitors or logged-in users. +* Added: WooCommerce honeytoken coupons — a hidden decoy promo code is planted on cart/checkout pages where coupon-scraping bots look but no human ever sees it. Applying it (classic checkout or Blocks/Store API) is a deterministic bot signal: recorded and, per your blocking setting, blocked. Optional daily rotation with a grace window; real coupons and shoppers are never affected. * Added: WordPress-native traps — fake vulnerable-plugin paths (armed only for plugins not actually installed, so real plugins are never shadowed), optional XML-RPC probing trap (off by default), and author-enumeration protection: ?author=N and REST user enumeration return a canary username instead of leaking real ones, and a later login attempt with that canary is flagged as a critical exfiltration detection. * Added: Deceptive tripwire responses — a tripwire hit can serve a 404, believable fake content (fake .env / wp-config / SQL dump / phpinfo seeded with unique per-site canary credentials), or a slow-drip tarpit, instead of a plain 403. A later login attempt using a canary credential is logged as a critical exfiltration detection and blocked. Decoy content is template-only and never exposes real configuration. * Added: wd_clearance forwarding — a tripwire hit carrying the visitor's wd_clearance cookie is reported so the WebDecoy Cloud can durably deny the actor's device fingerprint (rotation-proof lockout). Heuristic rules never carry the token. diff --git a/includes/class-webdecoy-woocommerce.php b/includes/class-webdecoy-woocommerce.php index 8b62d41..81b3686 100644 --- a/includes/class-webdecoy-woocommerce.php +++ b/includes/class-webdecoy-woocommerce.php @@ -518,8 +518,109 @@ public function get_suspicious_ips(int $threshold = 3): array $threshold ), ARRAY_A) ?: []; } + + /** + * The honeytoken coupon code for this site — a fake promo code planted where + * coupon-scraping bots look but no human ever sees. Deterministic from a + * per-site secret; optionally rotates daily. Applying it is, by + * construction, an automated action. + */ + public function honeytoken_coupon(bool $rotate = false): string + { + $secret = get_option('webdecoy_coupon_secret', ''); + if (!is_string($secret) || $secret === '') { + $secret = bin2hex(random_bytes(16)); + add_option('webdecoy_coupon_secret', $secret, '', 'yes'); + } + $label = $rotate ? ('day:' . gmdate('Y-m-d')) : 'stable'; + return 'WD' . strtoupper(substr(hash_hmac('sha256', $label, $secret), 0, 8)); + } + + /** + * Hidden markup exposing the honeytoken coupon to page scrapers only. Placed + * in an HTML comment plus an offscreen, aria-hidden node so it never appears + * to a human or in the accessibility tree. + */ + public function render_coupon_bait(): void + { + if (empty($this->options['woo_honeytoken_coupons'])) { + return; + } + $code = $this->honeytoken_coupon(!empty($this->options['honeytoken_rotate'])); + echo "\n\n"; + echo '' . "\n"; + } + + /** + * Coupon-load filter: if the code being applied is our honeytoken, record a + * detection (and optionally block), then report it invalid. Runs for both + * classic checkout and the Store API / Blocks, since both load coupons via + * WC_Coupon → this filter. Real coupons are untouched. + * + * @param mixed $data Coupon data (false when no matching coupon post). + * @param string $code The coupon code being looked up. + * @return mixed + */ + public function catch_coupon($data, string $code) + { + if (empty($this->options['woo_honeytoken_coupons'])) { + return $data; + } + + $canary = $this->honeytoken_coupon(!empty($this->options['honeytoken_rotate'])); + // Also honor the previous day's code during rotation grace. + $prev = null; + if (!empty($this->options['honeytoken_rotate'])) { + $secret = get_option('webdecoy_coupon_secret', ''); + if (is_string($secret) && $secret !== '') { + $prev = 'WD' . strtoupper(substr(hash_hmac('sha256', 'day:' . gmdate('Y-m-d', time() - DAY_IN_SECONDS), $secret), 0, 8)); + } + } + + $normalized = strtoupper(trim($code)); + if ($normalized === $canary || ($prev !== null && $normalized === $prev)) { + $ip = $this->get_client_ip(); + $this->log_detection($ip, 'honeytoken_coupon', 100); + + if (($this->options['block_action'] ?? 'block') === 'block') { + $this->blocker->block( + $ip, + 'Honeytoken coupon applied', + ($this->options['block_duration'] ?? 24) > 0 ? $this->options['block_duration'] : null + ); + } + + return false; // reject as a non-existent coupon + } + + return $data; + } } +// Honeytoken coupons: plant a hidden fake code and catch anyone who applies it. +add_action('woocommerce_before_cart', function () { + $options = get_option('webdecoy_options', []); + if (empty($options['protect_checkout']) || empty($options['woo_honeytoken_coupons'])) { + return; + } + (new WebDecoy_WooCommerce($options))->render_coupon_bait(); +}); +add_action('woocommerce_before_checkout_form', function () { + $options = get_option('webdecoy_options', []); + if (empty($options['protect_checkout']) || empty($options['woo_honeytoken_coupons'])) { + return; + } + (new WebDecoy_WooCommerce($options))->render_coupon_bait(); +}); +add_filter('woocommerce_get_shop_coupon_data', function ($data, $code) { + $options = get_option('webdecoy_options', []); + if (empty($options['protect_checkout']) || empty($options['woo_honeytoken_coupons'])) { + return $data; + } + return (new WebDecoy_WooCommerce($options))->catch_coupon($data, (string) $code); +}, 10, 2); + // Hook into WooCommerce payment completion - track success add_action('woocommerce_payment_complete', function ($order_id) { $options = get_option('webdecoy_options', []); diff --git a/webdecoy.php b/webdecoy.php index c9ff333..ff63430 100644 --- a/webdecoy.php +++ b/webdecoy.php @@ -255,6 +255,9 @@ private function load_options(): void 'protect_checkout' => true, 'checkout_velocity_limit' => 5, 'checkout_velocity_window' => 3600, + // Plant a hidden honeytoken coupon; applying it is a deterministic + // bot signal (no human ever sees the code). + 'woo_honeytoken_coupons' => true, // Client-side Scanner 'scanner_enabled' => true, @@ -1834,6 +1837,7 @@ public function sanitize_options(array $input): array $sanitized['protect_checkout'] = !empty($input['protect_checkout']); $sanitized['checkout_velocity_limit'] = max(1, intval($input['checkout_velocity_limit'] ?? 5)); $sanitized['checkout_velocity_window'] = max(60, intval($input['checkout_velocity_window'] ?? 3600)); + $sanitized['woo_honeytoken_coupons'] = !empty($input['woo_honeytoken_coupons']); // Proof-of-Work $sanitized['pow_enabled'] = !empty($input['pow_enabled']);