Aug 31, 2026BTA Team8 min read

The Donation Form Was the RCE: How CVE-2026-82222 Chains Three Small Mistakes in GiveWP Into Unauthenticated Code Execution

A donation form is the last place most SOCs are looking for unauthenticated remote code execution. It sits on the marketing site, gets a code review once a year, and if it collects card data at all it hands the sensitive part off to Stripe. The plugin behind it is a business tool, not a router, not a hypervisor, not an appliance the security team owns. It runs somewhere in the marketing WordPress instance and nobody thinks about it until someone tries to donate and it errors.

Which is why we at BlueTeamAutomation have been sitting with CVE-2026-82222 this week. The bug is not in the payment flow, not in a rarely visited admin path, but in the ordinary give-and-take of the donation form itself. It turns any public-facing GiveWP site with one published form and one active gateway into an unauthenticated code execution target on the same PHP-FPM process that serves your homepage.

The three little wrong turns that chain into unauthenticated RCE

Look at the CVSS 10.0 score and it is easy to assume one catastrophic bug. GiveWP 4.16.7.2, the fix released on August 27, actually plugs three separate mistakes that together compose an unauthenticated end-to-end chain. Patchstack researcher Udin Chan reported it on July 28 and it landed public alongside the fix (BleepingComputer).

The first flaw is a registration action the plugin exposes for its donor-account feature; it happily creates accounts even when WordPress's own users_can_register option is switched off. A second flaw sits in the plugin's safeUnserialize helper that rehydrates donation session state and is not actually safe against attacker-controlled input. Rounding it out, a PHP gadget chain in bundled libraries turns a rehydrated object into a shell command. Chained, they let any anonymous visitor with reach to a published donation form plant a payload and land arbitrary OS commands the next time a front-end page renders.

Bypassing users_can_register with give_action=user_register

The starting position of the chain is that WordPress installs, by default, refuse public account creation. users_can_register is off. Site owners rely on that setting the same way they rely on a firewall accepting SSH from a jump host and nothing else.

GiveWP ships a give_action=user_register path reachable through admin-ajax.php because donors sometimes want to save address details for next time. Reasonable feature. The bug is that the registration handler was written for GiveWP's own opinion of when registration is allowed and never consulted the WordPress core option (SC Media). Public sites with users_can_register set to false still accept an account creation from any anonymous POST that includes the give_action parameter.

For a defender that already believed public registration was off on the marketing site, this alone is worth logging as an incident. It also means the attacker starts the rest of the chain with a real WordPress user record and a wp_usermeta row they can write into.

safeUnserialize wasn't

Here the bug turns into a well-loved bug class. GiveWP's donation flow persists session-shaped state through PHP's serialize() and rehydrates it later with a wrapper the plugin calls safeUnserialize. Wrappers of this shape usually gate on class names or refuse to instantiate objects at all through unserialize($data, ['allowed_classes' => false]).

The GiveWP helper allowed objects. It gated on class-name checks that a shaped payload could satisfy, and it fed data that had passed through the donation form into a real PHP unserialize() call (The Hacker News). Once you can send serialized data into unserialize() and have the receiver deserialize an object of a class you influence, you are in classic PHP object injection territory, the bug class CWE-502 exists to name.

The payload sleeps in the database until any page loads

The operationally interesting piece is where the payload lives between when the attacker submits it and when the server executes it. GiveWP writes the serialized string to the database as a normal part of donation session storage; the wrapping donation transaction can be benign or fail cleanly, and the row still exists.

Rehydration happens on the next front-end pageload of any GiveWP-touched page, which on a busy site is usually within seconds. A PHP object gets constructed from the stored serialized string, the class's destructor runs when the request lifecycle ends, and if that destructor terminates in a bundled library's method that eventually reaches an exec, passthru, or system sink, the shell command runs as the PHP worker.

Two consequences follow. First, the exploit does not need to complete inside the attacker's own request; the attacker leaves and the code runs on someone else's pageview, so a "weird outbound activity right after a suspicious inbound POST" heuristic will not fire in the same second. Second, cleanup means finding and removing the poisoned rows, not just applying the patch, because the payload keeps triggering until the row is gone.

What defenders can actually see

Detection here is not glamorous but it is tractable. On the wire and in your reverse proxy or WAF logs, you have three signals worth stacking.

Watch for any HTTP POST that includes give_action=user_register as a form or query parameter, especially from an IP that has never posted a donation before. On a WordPress config where users_can_register is false this request category should not exist at all; when it does, it maps cleanly to MITRE ATT&CK T1136.001. Correlate against subsequent donation-flow POSTs from the same session that carry the tell-tale bytes of a serialized PHP object: O: followed by an integer and a class name, or a:{s: early in the body. The content-type is application/x-www-form-urlencoded, so those markers are URL-encoded (O%3A and friends) inside a large form body where many WAF rules miss them; that step maps to T1190. Failed attempts also leave a third signal: PHP-FPM or WordPress errors and 500 responses that name unserialize, an "unexpected class" fatal, or an autoload failure inside a GiveWP path.

On the host, the observable footprint is a stored-XSS-shaped smell in a place stored XSS never usually lives. Query wp_options, wp_usermeta, and any GiveWP-owned meta or session table for serialized values whose class name is not from WordPress core, WooCommerce, or GiveWP's own listed classes. A one-liner over the WordPress database against meta_value or session payloads starting with O: and a class name not on your allowlist is fast and specific.

Once the destructor runs you are looking at T1059 executed as the PHP worker user. EDR that trusts PHP-FPM to spawn sh because it is normal plugin behavior on some sites will miss this cleanly; scope PHP-FPM parent-of-shell alerts to hosts running GiveWP-touched sites and the noise drops to something a small team can look at.

Patch, purge, and verify the object never lands

The patch is GiveWP 4.16.7.2, which blocks serialized data during donation processing and restricts object creation at several deserialization points. Update every WordPress instance running GiveWP 4.16.7.1 or earlier, including staging and marketing subsites the security team may not touch monthly. If you cannot patch inside your normal window, temporarily disable all published donation forms and the active payment gateways; the chain requires a live form to reach the storage step.

After patching, do three things you would not usually do for a GiveWP advisory. Audit wp_users for accounts created between July 28 and the day you patched, cross-reference against your real donor records, and clean up any that were not real. Grep the WordPress database for stored serialized payloads whose class names are not on your allowlist and null them out. Confirm the patch actually held by attempting the same chain against a canary site: an anonymous give_action=user_register should fail when users_can_register is false, and any donation-flow POST containing serialized bytes should be rejected before it hits the database. WPScan's plugin page keeps the running history of GiveWP object-injection variants, because the same class of bug finds new places to land.

None of this is a one-time exercise. Any plugin that touches donation flows, form state, or session storage is now on our watchlist for the same shape of bug, because the "safe unserialize but not really" pattern is more common than the ecosystem admits. This is exactly the loop BlueTeamAutomation automates end to end with BASzy: continuous breach and attack simulation of the exact chain, correlated against your EDR and SIEM, so the detections you wired up today keep working when the next variant lands.

Continuously validate your WordPress and web-app detections

BASzy replays the exact registration bypass and object-injection chain against your defenses, correlates the result with your EDR and SIEM, and proves your hunts still fire the next time a variant ships.

Explore BASzy →