Skip to content

Commit ba5674e

Browse files
authored
Merge pull request #30 from WebDecoy/feat/woocommerce-deception
feat: WooCommerce honeytoken coupons (#14)
2 parents ca18093 + 0d54ae6 commit ba5674e

4 files changed

Lines changed: 117 additions & 0 deletions

File tree

admin/partials/settings-page.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,17 @@
636636
</label>
637637
</td>
638638
</tr>
639+
<tr>
640+
<th scope="row"><?php esc_html_e('Honeytoken Coupon', 'webdecoy'); ?></th>
641+
<td>
642+
<label>
643+
<input type="checkbox" name="webdecoy_options[woo_honeytoken_coupons]" value="1"
644+
<?php checked($options['woo_honeytoken_coupons'] ?? true); ?> />
645+
<?php esc_html_e('Plant a hidden decoy coupon code', 'webdecoy'); ?>
646+
</label>
647+
<p class="description"><?php esc_html_e('A fake promo code is placed on cart/checkout pages where coupon-scraping bots look, but hidden from human shoppers. Applying it is a deterministic bot signal — recorded and (per your blocking setting) blocked. Zero false positives: no human ever sees the code.', 'webdecoy'); ?></p>
648+
</td>
649+
</tr>
639650
<tr>
640651
<th scope="row"><?php esc_html_e('Velocity Limit', 'webdecoy'); ?></th>
641652
<td>

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Added: Rule engine — deterministic rules evaluated before heuristic scoring; first DENY/THROTTLE wins, with dry-run (log without blocking). Parity with @webdecoy/node.
1010
* 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.
1111
* 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.
12+
* 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.
1213
* 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.
1314
* 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.
1415
* 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.

includes/class-webdecoy-woocommerce.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,8 +518,109 @@ public function get_suspicious_ips(int $threshold = 3): array
518518
$threshold
519519
), ARRAY_A) ?: [];
520520
}
521+
522+
/**
523+
* The honeytoken coupon code for this site — a fake promo code planted where
524+
* coupon-scraping bots look but no human ever sees. Deterministic from a
525+
* per-site secret; optionally rotates daily. Applying it is, by
526+
* construction, an automated action.
527+
*/
528+
public function honeytoken_coupon(bool $rotate = false): string
529+
{
530+
$secret = get_option('webdecoy_coupon_secret', '');
531+
if (!is_string($secret) || $secret === '') {
532+
$secret = bin2hex(random_bytes(16));
533+
add_option('webdecoy_coupon_secret', $secret, '', 'yes');
534+
}
535+
$label = $rotate ? ('day:' . gmdate('Y-m-d')) : 'stable';
536+
return 'WD' . strtoupper(substr(hash_hmac('sha256', $label, $secret), 0, 8));
537+
}
538+
539+
/**
540+
* Hidden markup exposing the honeytoken coupon to page scrapers only. Placed
541+
* in an HTML comment plus an offscreen, aria-hidden node so it never appears
542+
* to a human or in the accessibility tree.
543+
*/
544+
public function render_coupon_bait(): void
545+
{
546+
if (empty($this->options['woo_honeytoken_coupons'])) {
547+
return;
548+
}
549+
$code = $this->honeytoken_coupon(!empty($this->options['honeytoken_rotate']));
550+
echo "\n<!-- promo code: " . esc_html($code) . " -->\n";
551+
echo '<div aria-hidden="true" style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden">'
552+
. '<span class="wd-promo" data-coupon="' . esc_attr($code) . '">' . esc_html($code) . '</span></div>' . "\n";
553+
}
554+
555+
/**
556+
* Coupon-load filter: if the code being applied is our honeytoken, record a
557+
* detection (and optionally block), then report it invalid. Runs for both
558+
* classic checkout and the Store API / Blocks, since both load coupons via
559+
* WC_Coupon → this filter. Real coupons are untouched.
560+
*
561+
* @param mixed $data Coupon data (false when no matching coupon post).
562+
* @param string $code The coupon code being looked up.
563+
* @return mixed
564+
*/
565+
public function catch_coupon($data, string $code)
566+
{
567+
if (empty($this->options['woo_honeytoken_coupons'])) {
568+
return $data;
569+
}
570+
571+
$canary = $this->honeytoken_coupon(!empty($this->options['honeytoken_rotate']));
572+
// Also honor the previous day's code during rotation grace.
573+
$prev = null;
574+
if (!empty($this->options['honeytoken_rotate'])) {
575+
$secret = get_option('webdecoy_coupon_secret', '');
576+
if (is_string($secret) && $secret !== '') {
577+
$prev = 'WD' . strtoupper(substr(hash_hmac('sha256', 'day:' . gmdate('Y-m-d', time() - DAY_IN_SECONDS), $secret), 0, 8));
578+
}
579+
}
580+
581+
$normalized = strtoupper(trim($code));
582+
if ($normalized === $canary || ($prev !== null && $normalized === $prev)) {
583+
$ip = $this->get_client_ip();
584+
$this->log_detection($ip, 'honeytoken_coupon', 100);
585+
586+
if (($this->options['block_action'] ?? 'block') === 'block') {
587+
$this->blocker->block(
588+
$ip,
589+
'Honeytoken coupon applied',
590+
($this->options['block_duration'] ?? 24) > 0 ? $this->options['block_duration'] : null
591+
);
592+
}
593+
594+
return false; // reject as a non-existent coupon
595+
}
596+
597+
return $data;
598+
}
521599
}
522600

601+
// Honeytoken coupons: plant a hidden fake code and catch anyone who applies it.
602+
add_action('woocommerce_before_cart', function () {
603+
$options = get_option('webdecoy_options', []);
604+
if (empty($options['protect_checkout']) || empty($options['woo_honeytoken_coupons'])) {
605+
return;
606+
}
607+
(new WebDecoy_WooCommerce($options))->render_coupon_bait();
608+
});
609+
add_action('woocommerce_before_checkout_form', function () {
610+
$options = get_option('webdecoy_options', []);
611+
if (empty($options['protect_checkout']) || empty($options['woo_honeytoken_coupons'])) {
612+
return;
613+
}
614+
(new WebDecoy_WooCommerce($options))->render_coupon_bait();
615+
});
616+
add_filter('woocommerce_get_shop_coupon_data', function ($data, $code) {
617+
$options = get_option('webdecoy_options', []);
618+
if (empty($options['protect_checkout']) || empty($options['woo_honeytoken_coupons'])) {
619+
return $data;
620+
}
621+
return (new WebDecoy_WooCommerce($options))->catch_coupon($data, (string) $code);
622+
}, 10, 2);
623+
523624
// Hook into WooCommerce payment completion - track success
524625
add_action('woocommerce_payment_complete', function ($order_id) {
525626
$options = get_option('webdecoy_options', []);

webdecoy.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ private function load_options(): void
255255
'protect_checkout' => true,
256256
'checkout_velocity_limit' => 5,
257257
'checkout_velocity_window' => 3600,
258+
// Plant a hidden honeytoken coupon; applying it is a deterministic
259+
// bot signal (no human ever sees the code).
260+
'woo_honeytoken_coupons' => true,
258261

259262
// Client-side Scanner
260263
'scanner_enabled' => true,
@@ -1834,6 +1837,7 @@ public function sanitize_options(array $input): array
18341837
$sanitized['protect_checkout'] = !empty($input['protect_checkout']);
18351838
$sanitized['checkout_velocity_limit'] = max(1, intval($input['checkout_velocity_limit'] ?? 5));
18361839
$sanitized['checkout_velocity_window'] = max(60, intval($input['checkout_velocity_window'] ?? 3600));
1840+
$sanitized['woo_honeytoken_coupons'] = !empty($input['woo_honeytoken_coupons']);
18371841

18381842
// Proof-of-Work
18391843
$sanitized['pow_enabled'] = !empty($input['pow_enabled']);

0 commit comments

Comments
 (0)