Skip to content

fix: card-testing evidence inflation, the dead intel store, and the monitor counter - #66

Merged
cport1 merged 1 commit into
mainfrom
fix/61-62-63
Jul 30, 2026
Merged

fix: card-testing evidence inflation, the dead intel store, and the monitor counter#66
cport1 merged 1 commit into
mainfrom
fix/61-62-63

Conversation

@cport1

@cport1 cport1 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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_processed fires on every submission and WC_Checkout::create_order() reuses the order in the order_awaiting_payment session. A customer retrying a declined card produced several rows against one order_id and could trip Patterns 1, 2 and 4 on their own.

Those three now require evidence one order cannot fabricate: >= 3 distinct orders or >= 2 distinct 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 single order_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:

  • "already blocked network-wide", a success chip linking to a blocked-IPs page that no longer holds feed rows
  • a Pro upsell claiming the actor "would have been blocked before its first request"

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(), the blocker() accessor and its property, EXPIRY_HOURS. Corrected the class docblock (still described mirroring into the blocked-IPs table) and recorded that exclude_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_limits and a detections row, so no new order of cost. Docblock said "autoloaded counter" while the call passes autoload=false; corrected.


80/80 tests, phpcs error count unchanged (14, all pre-existing in tests/bootstrap.php), phpstan unchanged.

…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.
@cport1
cport1 merged commit 4b38616 into main Jul 30, 2026
3 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

track_attempt() records one row per checkout POST, so a customer retrying a declined card manufactures the card-testing evidence

1 participant