fix: card-testing evidence inflation, the dead intel store, and the monitor counter - #66
Merged
Conversation
…onitor counter (#61 #62 #63) #61 — one reused order could manufacture a card-testing verdict. track_attempt() records a row per checkout SUBMISSION, not per order: woocommerce_checkout_order_processed fires on every submission and WC_Checkout::create_order() reuses the order held in the order_awaiting_payment session. So a customer retrying a declined card produced several rows against one order_id and could trip Patterns 1, 2 and 4 alone. Those three now require evidence one order cannot fabricate — three distinct orders or more than one card. Pattern 3 already keys on distinct cards and is unchanged. Deliberately NOT fixed by deduping rows per order_id: a real carding bot cycling cards through one reused cart also produces a single order_id, so a dedupe would collapse the attack to one row and blind every pattern plus the velocity counter. #62 — the actor feed was a write-only dead store. record_intel() wrote an option and intel_for() had zero callers, so 2.3.2 removed the enforcement and replaced it with nothing, which deleted the feature rather than re-scoping it. intel_for() is now the primary source for the CRITICAL-moment notice, which was paying for a blocking HTTP lookup on a page render; the live lookup is the fallback when the local feed has nothing. Two notices also asserted things that stopped being true when the feed stopped blocking — "already blocked network-wide" with a success chip linking to a blocked-IPs page that no longer holds feed rows, and a Pro upsell claiming the actor "would have been blocked before its first request". Both now describe recognition rather than a block. Removed the block-era carcass: enforce_cap(), the blocker() accessor and its property, EXPIRY_HOURS. Corrected the class docblock, which still described mirroring the feed into the blocked-IPs table, and recorded that exclude_allowlisted() is exact-match only — acceptable while the store is advisory, not if a blocking consumer ever returns. #63 — the monitor-mode notice could read "no request has met the bar for enforcement yet" on a default install where rate limiting was the only thing being suppressed, because THROTTLE was excluded from the counter. Now counted, keyed on the rule name rather than the reason: a throttle reason embeds the request counter and would mint a fresh breakdown key per request. A throttled request already writes webdecoy_rate_limits and inserts a detections row, so this adds no new order of cost. Also corrected the docblock that said "autoloaded counter" when the call passes autoload=false. 80/80 tests, phpcs error count unchanged (14, all pre-existing in tests/bootstrap.php), phpstan unchanged.
This was referenced Jul 30, 2026
Merged
cport1
added a commit
that referenced
this pull request
Jul 30, 2026
Version bump plus changelog/readme for the four WooCommerce and reporting fixes merged as #65 and #66 (issues #60, #61, #62, #63). The 2.3.2 changelog carried a stated limitation — that the card-testing fix did not apply to Cash on Delivery, Bank Transfer or Cheque stores. That limitation is now resolved, so the entry says so rather than leaving the caveat standing.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #61, #62, #63.
#61 — one reused order could manufacture a card-testing verdict
track_attempt()records a row per checkout submission, not per order —woocommerce_checkout_order_processedfires on every submission andWC_Checkout::create_order()reuses the order in theorder_awaiting_paymentsession. A customer retrying a declined card produced several rows against oneorder_idand could trip Patterns 1, 2 and 4 on their own.Those three now require evidence one order cannot fabricate:
>= 3distinct orders or>= 2distinct cards. Pattern 3 already keys on distinct cards and is untouched.Not fixed by deduping per
order_id, which the review explicitly warned against: a real carding bot cycling cards through one reused cart also produces a singleorder_id, so a dedupe would collapse the attack to one row and blind every pattern and the velocity counter.#62 — the actor feed was a write-only dead store
record_intel()wrote an option;intel_for()had zero callers. 2.3.2 removed the enforcement and replaced it with nothing, which deleted the feature rather than re-scoping it.intel_for()is now the primary source for the CRITICAL-moment notice — which was paying for a blocking HTTP lookup on a page render — with the live lookup kept as fallback when the local feed has nothing.Two notices also asserted things that stopped being true when the feed stopped blocking:
Both now describe recognition, which is real, rather than a block, which does not happen on any plan.
Removed the block-era carcass:
enforce_cap(), theblocker()accessor and its property,EXPIRY_HOURS. Corrected the class docblock (still described mirroring into the blocked-IPs table) and recorded thatexclude_allowlisted()is exact-match only — fine while the store is advisory, not if a blocking consumer ever returns.#63 — the monitor counter omitted the thing most likely to fire
The notice could read "no request has met the bar for enforcement yet" on a default install where rate limiting was the only thing suppressed. THROTTLE is now counted, keyed on the rule name not the reason — a throttle reason embeds the request counter and would mint a fresh breakdown key per request. A throttled request already writes
webdecoy_rate_limitsand a detections row, so no new order of cost. Docblock said "autoloaded counter" while the call passesautoload=false; corrected.80/80 tests, phpcs error count unchanged (14, all pre-existing in
tests/bootstrap.php), phpstan unchanged.