woocommerce.php
WooCommerce integration.
Protects four WooCommerce forms — Checkout, My-Account login, registration
and lost-password — when the master toggle protect_woocommerce and the
respective sub-toggle are on. Reviews are out of scope: they use the
WordPress comment system and are already covered by protect_comments.
Lost-Password is render-only: WooCommerce submits the lost-password form
through WordPress core's retrieve_password(), which fires
lostpassword_post. Our existing core module
(includes/forms/password-reset.php) hooks that action — adding another
verify here would just produce duplicate verification and duplicate events.
protect_wc_lost_password (render) and protect_password_reset (verify)
are independent toggles — see IN-5 below for the resulting split states,
surfaced via wp creacaptcha doctor.
Known, deliberately-scoped gaps (Modul 27 security audit, 2026-07-29):
- IN-2 (P0): the WooCommerce Block Checkout (Store API route) never calls
process_checkout(), so neither the render hook (woocommerce_review_order_before_submit) nor the verify hook (woocommerce_after_checkout_validation) below ever runs for it — a Block Checkout is completely unprotected regardless ofprotect_wc_checkout. Building Store-API protection is explicitly OUT OF SCOPE here (new functionality, its own module per the audit's Phase-2 plan);creationell_captcha_wc_uses_block_checkout()below only detects the gap sowp creacaptcha doctorand the Settings help text can stop giving a false "protected" impression. - IN-3/IN-4: the classic checkout's account-creation branch and the
standalone My-Account registration form both funnel through
woocommerce_registration_errors(fired unconditionally bywc_create_new_customer()) — see the per-request verify cache and the form-context guard below. - IN-12 (P3, fail-closed): all four render hooks above sit in overridable
WooCommerce templates (
payment.php,form-login.php,form-lost-password.php,global/form-login.php), while every verify runs from core PHP hooks that do not depend on the template at all. A theme that copies one of these templates and drops the correspondingdo_action()call loses the widget but NOT the server-side check — the form fails closed (no spam bypass) rather than open, but every submission is then rejected. This is a general theme-customization risk common to any WooCommerce hook-based extension, not fixable from inside this plugin; documented here rather than "fixed".
Table of Contents
Functions
- creationell_captcha_woocommerce_active() : bool
- Whether any WooCommerce protection applies right now.
- creationell_captcha_wc_uses_block_checkout() : bool
- Whether the currently configured WooCommerce checkout page uses the Block Checkout (the `woocommerce/checkout` block) rather than the classic `[woocommerce_checkout]` shortcode.
- creationell_captcha_wc_checkout_active() : bool
- Whether the WooCommerce checkout protection is active.
- creationell_captcha_wc_checkout_render() : void
- Renders the widget directly before the Place-Order button on the checkout.
- creationell_captcha_wc_checkout_verify_cache() : bool|null
- Per-request cache of the checkout's own verify verdict, keyed by the raw `altcha` payload currently in `$_POST`.
- creationell_captcha_wc_checkout_verify() : void
- Verifies the captcha during checkout validation.
- creationell_captcha_wc_login_active() : bool
- Whether the WooCommerce my-account login protection is active.
- creationell_captcha_wc_login_render() : void
- Renders the widget at the bottom of the WooCommerce login form.
- creationell_captcha_wc_login_verify() : mixed
- Verifies the captcha on a WooCommerce my-account login submission.
- creationell_captcha_wc_registration_active() : bool
- Whether the WooCommerce registration protection is active.
- creationell_captcha_wc_registration_render() : void
- Renders the widget at the bottom of the WooCommerce registration form.
- creationell_captcha_wc_nonce_verifies() : bool
- Reads a nonce value the way the given WooCommerce field name(s) would, replicating WooCommerce core's own field-name-agnostic fallback: the dedicated field wins if present, otherwise the generic `_wpnonce` field is used. WooCommerce's own nonce checks do not care WHICH field carried the value — only whether the value itself verifies against the action name.
- creationell_captcha_wc_registration_form_context() : bool
- Whether the current `wc_create_new_customer()` call originates from a genuine WooCommerce registration- or checkout-form POST, as opposed to a Store-API, programmatic or CLI call that never had a form (and therefore no `altcha` field) in the first place.
- creationell_captcha_wc_registration_verify() : mixed
- Verifies the captcha during WooCommerce my-account registration.
- creationell_captcha_wc_lost_password_active() : bool
- Whether the WooCommerce lost-password render is active.
- creationell_captcha_wc_lost_password_render() : void
- Renders the widget inside the WooCommerce lost-password form.
Functions
creationell_captcha_woocommerce_active()
Whether any WooCommerce protection applies right now.
creationell_captcha_woocommerce_active() : bool
Shared gate that the per-form predicates _wc_*_active() route through —
encapsulates the kill-switch, the class_exists check and the master
toggle so each form predicate just needs to add its own sub-toggle check.
Return values
boolcreationell_captcha_wc_uses_block_checkout()
Whether the currently configured WooCommerce checkout page uses the Block Checkout (the `woocommerce/checkout` block) rather than the classic `[woocommerce_checkout]` shortcode.
creationell_captcha_wc_uses_block_checkout() : bool
IN-2: this codebase only ever protects the classic checkout — the render
hook (woocommerce_review_order_before_submit) and the verify hook
(woocommerce_after_checkout_validation) both fire exclusively from
WC_Checkout::process_checkout(), which the Block Checkout's Store API
route never calls. This function exists purely to DETECT that gap for
wp creacaptcha doctor and the Settings help text — it deliberately does
NOT attempt to inject the widget or hook the Store API route (out of
scope per the audit's Phase-2 plan; that would be new functionality
belonging to its own module).
Return values
boolcreationell_captcha_wc_checkout_active()
Whether the WooCommerce checkout protection is active.
creationell_captcha_wc_checkout_active() : bool
Return values
boolcreationell_captcha_wc_checkout_render()
Renders the widget directly before the Place-Order button on the checkout.
creationell_captcha_wc_checkout_render() : void
creationell_captcha_wc_checkout_verify_cache()
Per-request cache of the checkout's own verify verdict, keyed by the raw `altcha` payload currently in `$_POST`.
creationell_captcha_wc_checkout_verify_cache([bool|null $set = null ]) : bool|null
IN-3: the classic checkout, when the customer checks "create an
account?", posts exactly ONE altcha payload but runs it through TWO of
our verify hooks within the same request —
creationell_captcha_wc_checkout_verify() first (inside
WC_Checkout::validate_checkout()), then
creationell_captcha_wc_registration_verify() a few lines later (inside
WC_Checkout::process_customer() → wc_create_new_customer()), reading
the IDENTICAL $_POST['altcha']. The underlying ALTCHA challenge is
single-use (class-engine.php's replay-transient marker), so the second
check would always find the payload already marked used and fail — an
account-creating order could never succeed. Recording the checkout
verdict here lets the registration verify reuse it for the same payload
instead of re-running the used-marker check a second time.
Scoped to this file/request only (static, resets every request) — it
does not weaken replay protection across requests: a genuinely replayed
payload from an EARLIER request still hits the used-marker on its very
first check here, because this cache starts empty every time.
Parameters
- $set : bool|null = null
-
Pass a bool to record the verdict for the current raw payload; omit (null, default) to just read a previously recorded one.
Return values
bool|null —The cached verdict for the current raw payload, or
null if none has been recorded yet this request.
creationell_captcha_wc_checkout_verify()
Verifies the captcha during checkout validation.
creationell_captcha_wc_checkout_verify(array<string, mixed> $data, mixed $errors) : void
woocommerce_after_checkout_validation fires inside WooCommerce's
process_checkout() after all other validation has run; adding an error
to the passed-through WP_Error aborts the order.
Parameters
- $data : array<string, mixed>
-
Posted checkout data (unused).
- $errors : mixed
-
The checkout
WP_Error(passed by reference of the object).
creationell_captcha_wc_login_active()
Whether the WooCommerce my-account login protection is active.
creationell_captcha_wc_login_active() : bool
Return values
boolcreationell_captcha_wc_login_render()
Renders the widget at the bottom of the WooCommerce login form.
creationell_captcha_wc_login_render() : void
creationell_captcha_wc_login_verify()
Verifies the captcha on a WooCommerce my-account login submission.
creationell_captcha_wc_login_verify(mixed $validation_error, string $username, string $password) : mixed
Returns a WP_Error to fail the login; otherwise returns the incoming
$validation_error value unchanged (so other filters can keep working).
Parameters
- $validation_error : mixed
-
The current validation error (
WP_Error|null|false). - $username : string
-
Submitted username (unused).
- $password : string
-
Submitted password (unused).
creationell_captcha_wc_registration_active()
Whether the WooCommerce registration protection is active.
creationell_captcha_wc_registration_active() : bool
Return values
boolcreationell_captcha_wc_registration_render()
Renders the widget at the bottom of the WooCommerce registration form.
creationell_captcha_wc_registration_render() : void
creationell_captcha_wc_nonce_verifies()
Reads a nonce value the way the given WooCommerce field name(s) would, replicating WooCommerce core's own field-name-agnostic fallback: the dedicated field wins if present, otherwise the generic `_wpnonce` field is used. WooCommerce's own nonce checks do not care WHICH field carried the value — only whether the value itself verifies against the action name.
creationell_captcha_wc_nonce_verifies(array<string, mixed> $source, string $dedicated_field, string $action) : bool
Review fix (Fix-Runde 1, IN-4 critical): an earlier version of this guard
checked the dedicated field's mere PRESENCE
(isset( $_POST['woocommerce-register-nonce'] )) as its "genuine form"
signal. That was exploitable — WooCommerce itself resolves the nonce
VALUE via the same _wpnonce fallback implemented here, so an anonymous
attacker could read the nonce value off the real registration form's
hidden field and resubmit register=…&email=…&_wpnonce=<value> while
OMITTING the woocommerce-register-nonce field. WooCommerce would still
accept the nonce via its fallback and create the account, while the old,
presence-only guard saw no dedicated field and treated the request as
"no form context" — skipping the captcha check entirely. Checking the
NONCE VALUE (via wp_verify_nonce()) instead of field presence closes
this: the fallback is followed identically on both sides, so there is no
field-name an attacker can omit to fool this guard while WooCommerce
itself still accepts the request.
Parameters
- $source : array<string, mixed>
-
$_POSTor$_REQUEST. - $dedicated_field : string
-
The form's own nonce field name.
- $action : string
-
The nonce action to verify against.
Return values
boolcreationell_captcha_wc_registration_form_context()
Whether the current `wc_create_new_customer()` call originates from a genuine WooCommerce registration- or checkout-form POST, as opposed to a Store-API, programmatic or CLI call that never had a form (and therefore no `altcha` field) in the first place.
creationell_captcha_wc_registration_form_context() : bool
IN-4: wc_create_new_customer() fires woocommerce_registration_errors
— the filter this module hooks — UNCONDITIONALLY, including for the
Woo Blocks Store API's own account-creation path and for direct
programmatic calls (wp eval, custom scripts, other plugins). Hard-
rejecting those whenever no altcha field happens to be present made
wc_create_new_customer() unusable outside an actual captcha-protected
form — the customer was simply never created.
Both genuine entry points already verify their OWN nonce, inside
WooCommerce core, before ever calling wc_create_new_customer() — so
independently re-verifying the SAME nonce value/action here (not
consuming anything, wp_verify_nonce() is read-only) is a safe, precise
signal, as long as it replicates WC's exact field-name fallback (see
creationell_captcha_wc_nonce_verifies() above — a presence-only check
was exploitable, see that function's docblock):
- Standalone My-Account registration form:
WC_Form_Handler::process_registration()gates onisset( $_POST['register'], $_POST['email'] )plus a validwoocommerce-registernonce (dedicated fieldwoocommerce-register-nonce, falling back to_wpnonce) before ever callingwc_create_new_customer(). - Classic checkout with "Create an account?":
WC_Checkout::process_checkout()verifies thewoocommerce-process_checkoutnonce (dedicated fieldwoocommerce-process-checkout-nonce, falling back to_wpnonce, read from$_REQUEST) beforevalidate_checkout()/process_customer()run — and by the time this filter fires from insideprocess_customer(), our OWN checkout verify (creationell_captcha_wc_checkout_verify()) has already run too, see IN-3's cache below.
CLI/cron/XML-RPC calls need no explicit exclusion here: none of them ever
populate $_POST/$_REQUEST with a nonce that verifies against either
action, so both checks below already resolve to false for them without
a dedicated SAPI/constant check — one less thing to keep in sync, and it
keeps this guard testable by directly setting $_POST (see tests/eval/).
Deliberately does NOT exclude on wp_doing_ajax(): WooCommerce's classic
checkout is itself normally submitted via wc-ajax=checkout
(WC_AJAX::checkout(), which defines DOING_AJAX itself before
dispatch) — excluding AJAX outright would silently re-open IN-3/IN-4 for
the majority of real-world checkouts.
Return values
boolcreationell_captcha_wc_registration_verify()
Verifies the captcha during WooCommerce my-account registration.
creationell_captcha_wc_registration_verify(mixed $errors, string $username, string $email) : mixed
Parameters
- $errors : mixed
-
The current
WP_Errorcarrier from WooCommerce. - $username : string
-
Submitted username (unused).
- $email : string
-
Submitted email (unused).
creationell_captcha_wc_lost_password_active()
Whether the WooCommerce lost-password render is active.
creationell_captcha_wc_lost_password_active() : bool
Return values
boolcreationell_captcha_wc_lost_password_render()
Renders the widget inside the WooCommerce lost-password form.
creationell_captcha_wc_lost_password_render() : void