OptinMonster supply chain attack: how one stolen key opened the door to 1.2 million WordPress sites
OptinMonster supply chain attack: how one stolen key opened the door to 1.2 million WordPress sites
Imagine your WordPress is fully up to date, every plugin is current, no vulnerabilities in your own code. An administrator logs into the dashboard — just another workday. A few hours later, a hidden account with administrator privileges appears on the site, along with a backdoor plugin carrying a web shell. Nobody broke into your server. The problem came from a trusted tool you were paying for.
On June 12, 2026, attackers carried out a supply chain attack targeting three popular WordPress plugins from Awesome Motive: OptinMonster (over 1.2 million active installs), TrustPulse, and PushEngage. The attack did not exploit vulnerabilities in the plugins themselves — it struck higher up the chain, at the CDN infrastructure through which these plugins deliver their JavaScript to customers. The attack was first discovered by Sansec, a firm specializing in e-commerce security. A detailed technical analysis was subsequently published by Patchstack.
There is no CVE number or CVSS score here. This is an attack on trust: the attackers exploited the fact that sites trust the JavaScript their plugin vendor delivers. And that trust was turned against them.
HOW THIS WAS POSSIBLE
The attackers did not break into OptinMonster. They did not hunt for vulnerabilities in the plugin code, did not brute-force the API, did not dig through WordPress internals. They walked in through the company’s own marketing website — a standard WordPress installation running UpdraftPlus with a known, unpatched vulnerability. Breaking into a plugin vendor’s marketing site is vastly easier than breaking into the plugin itself. And the prize waiting there turned out to be disproportionately large.
On that marketing server sat a CDN API key — the same CDN Awesome Motive uses to deliver JavaScript files to all of its customers, across all 1.2 million OptinMonster sites. One key. Full write access. The attacker did not need to compromise each site individually: they changed one file in one place, and the CDN distributed the poisoned script to every customer on its own. That is supply chain attack in its purest form — instead of picking a million locks, you compromise the one workshop that makes them all.
The attackers replaced the legitimate JavaScript SDKs with tampered versions on the following CDN domains:
a.omappapi.com/app/js/api.min.js (OptinMonster)
a.opmnstr.com/app/js/api.min.js (OptinMonster)
a.optnmstr.com/app/js/api.min.js (OptinMonster)
a.trstplse.com/app/js/api.min.js (TrustPulse)
clientcdn.pushengage.com/sdks/pushengage-web-sdk.js (PushEngage)
The malicious code did not replace the original — it was appended to the end of the legitimate minified SDK. The plugin continued working normally while the malicious logic ran alongside it. This is precisely why standard monitoring did not immediately raise an alarm.
HOW THE MALICIOUS CODE WORKED
The script was written with surgical precision — it activated only under the right conditions and avoided detection on multiple levels.
First, it checked the execution environment: it looked for navigator.webdriver, headless browser markers (window._phantom, window.__nightmare), and zero-size browser windows. This was protection against automated scanners and security researchers. It then confirmed it was running specifically in a WordPress administrator context by checking for the admin bar, /wp-admin/ paths, and the wordpress_logged_in_ cookie. If no administrator was present — it simply stopped. After triggering successfully, it stamped localStorage to avoid repeating on the same browser for 24 hours.
Having found an administrator, the script collected everything needed to act on their behalf: it determined the WordPress root and admin path, extracted a valid REST nonce from wpApiSettings, through admin-ajax.php?action=rest-nonce, or from the add-user page. With the administrator’s session and a valid nonce, all subsequent requests looked, at the network layer, like legitimate actions performed by the administrator themselves.
Rogue administrator creation happened through four parallel methods — the script tried them in sequence until one succeeded:
// Method 1: REST API
POST /wp-json/wp/v2/users
{"username": "dev_3m6nyp", "email": "[email protected]", "roles": ["administrator"]}
// Method 2: WordPress admin form
POST /wp-admin/user-new.php?action=createuser
// Method 3: AJAX
POST /wp-admin/admin-ajax.php (same payload)
// Method 4: hidden 1x1px iframe loading user-new.php
The script created both a fixed account developer_api1 with address [email protected], and randomized accounts following the pattern dev_xxxxxx / [email protected]. It even carried a built-in dictionary of “user already exists” messages in multiple languages — to correctly detect success in localized WordPress installations.
Once it had admin access, the script fetched a backdoor ZIP from a C2 server and installed it via POST /wp-admin/update.php?action=upload-plugin. The backdoor plugin changed its disguise: in different observations it masqueraded as “Content Delivery Helper” (content-delivery-helper, version 2.7.1) or “Database Optimizer” (database-optimizer, version 2.9.4). It also hid itself from the plugin list, update queue, and WordPress activity logs — spotting it through the dashboard was essentially impossible.
The installed backdoor opened two channels for remote code execution. The parameter ?developer_api1_fm activated a web shell executing system($_POST['cmd']). The parameter developer_api1_eval executed arbitrary base64-decoded PHP code. Collected data was XOR-encrypted with the key jX9kM2nP4qR6sT8v, base64-encoded, and beaconed to the domain tidio.cc (a deliberate lookalike of the legitimate tidio.com) through a resilient fallback chain: navigator.sendBeacon → fetch → XMLHttpRequest → image pixel.
REAL-WORLD ATTACK CHAIN
Patchstack’s telemetry confirmed the scale. Over 36 hours — June 14–15 — their WAF rule blocked 271 rogue administrator creation attempts across 13 sites, originating from 81 unique IP addresses. Breakdown by vector: 263 requests via REST API /wp-json/wp/v2/users, 5 via the /wp-admin/user-new.php form, 3 via /wp-admin/admin-ajax.php.
The nature of the traffic is particularly telling: 81 different IP addresses, roughly 60% mobile browsers (Android/Samsung), the rest Windows, macOS, and Linux. This was not a centralized attack from attacker-controlled servers. These were the browsers of real WordPress administrators, who became unwitting weapons in the attack. This is precisely why WAFs had such difficulty distinguishing malicious requests from legitimate ones: they carried valid sessions and valid nonces.
The tidio.cc domain was registered on April 28, 2026 — six weeks before the attack. A TLS certificate was obtained at the same time. The attack was planned well in advance.
TIMELINE
April 28, 2026: C2 domain tidio.cc registered and TLS certificate obtained — infrastructure preparation.
June 12, 2026, 22:17 UTC: malicious code first observed in OptinMonster and TrustPulse api.min.js files.
June 12, 2026, 22:42 UTC: last confirmed presence of malicious code on the OptinMonster and TrustPulse CDN. The exposure window was approximately 25 minutes on the primary CDN, but CDN edge caching meant some nodes continued serving the compromised script for longer.
June 13, 2026, 19:02 UTC: PushEngage SDK still serving injected code from some CDN edges.
June 14, 2026: malicious code removed from PushEngage CDN; Awesome Motive publishes official incident disclosure.
June 14–15, 2026: Patchstack actively blocks exploitation attempts across protected sites.
WHY THIS MATTERS
Everything updated. Plugins current. Server patched. And still compromised. That is what makes this attack genuinely uncomfortable — it does not hit a weak point in your defenses, it hits a blind spot. The standard “keep plugins updated and you’ll be fine” model simply does not apply here, because the vulnerability was not in the plugin but in the infrastructure delivering it. You could not see it, could not patch your way past it, and could not detect it with standard tooling.
There is another point worth understanding clearly: the requests that created the rogue administrator were not generated by the attacker’s server — they were generated by your own administrator’s browser. With their session, their nonce, their cookies. WordPress saw a legitimate user performing a legitimate action. This is exactly why most WAF rules stayed quiet: how do you block an administrator creating a user? It turns out you can — but only if you know the exact signatures of the specific attacker. Patchstack did exactly that, but only after the campaign was already in progress.
If your site runs WooCommerce, the stakes are higher still. Payment card data, customer addresses, order history, subscription tokens. A backdoor with full RCE on such a site is no longer just a technical incident. It is a data breach — with everything that follows: regulatory notification, customer notification, reputational damage.
HOW TO CHECK YOUR SITE
If OptinMonster, TrustPulse, or PushEngage was active on your site and an administrator logged into the dashboard between June 12–14, 2026, a check is essential. The WordPress dashboard is not reliable here — the backdoor actively hides itself from the admin UI, removing itself from the plugin list, update queue, and activity logs. Every meaningful check happens at the server filesystem level.
Start with the administrator account list. If you have WP-CLI, this is one command — it outputs the login and email of every user with the administrator role, newest first:
wp user list --role=administrator --fields=user_login,user_email
The command must run as the user who owns the WordPress files — otherwise WP-CLI cannot read wp-config.php. If you get a Permission denied error, check the file owner and use sudo -u:
# Check who owns the WordPress files
ls -la /var/www/html/wp-config.php
# Run as that user (typically www-data)
sudo -u www-data wp user list --role=administrator --fields=user_login,user_email --path=/var/www/html
Without WP-CLI, run the equivalent directly in MySQL. The query joins the users table with the usermeta table and filters for anyone with administrator in their capabilities:
SELECT user_login, user_email FROM wp_users
JOIN wp_usermeta ON wp_users.ID = wp_usermeta.user_id
WHERE meta_key = 'wp_capabilities'
AND meta_value LIKE '%administrator%'
ORDER BY user_registered DESC;
Look for developer_api1 / [email protected] and any accounts following the pattern dev_xxxxxx — six random characters after dev_. Either is a direct IoC.
Next, check the filesystem. The -la flags in ls show hidden files and directories (those starting with a dot), along with permissions and last-modified timestamps — useful for spotting recently created folders:
ls -la /var/www/html/wp-content/plugins/
The backdoor installs under content-delivery-helper or database-optimizer, but the name rotates — so look for any unfamiliar directory, especially ones created between June 12–14.
Even if nothing looks suspicious by name, search for the backdoor’s signatures directly in the code. The -r flag runs a recursive search through all files under the given path. The three commands below look for: the web shell parameter name, the arbitrary PHP execution parameter, and the XOR encryption key. If any returns output, the backdoor is present:
grep -r "developer_api1_fm" /var/www/html/wp-content/plugins/
grep -r "developer_api1_eval" /var/www/html/wp-content/plugins/
grep -r "jX9kM2nP4qR6sT8v" /var/www/html/wp-content/plugins/
If your site was active during the attack window, also block the C2 domain to cut off any residual outbound connections. The nftables option adds a rule to the output chain that drops all outgoing packets to IP 84.201.6.54. The /etc/hosts option redirects all DNS lookups for tidio.cc to a non-routable address — useful as a fallback if nftables is not in use:
# Option 1: nftables — packet-level block
nft add rule inet filter output ip daddr 84.201.6.54 drop
# Option 2: /etc/hosts — DNS-level block
echo "0.0.0.0 tidio.cc" | sudo tee -a /etc/hosts
If you find even one of these indicators, treat the site as fully compromised. Deleting the backdoor and rogue account is only the start — with RCE available, the attacker could have placed additional persistence anywhere on the filesystem. Rotate everything: administrator passwords, API keys, database credentials, and the security keys and salts in wp-config.php. The salts are what WordPress uses to sign session cookies; rotating them invalidates all current sessions immediately.
HOW TO PROTECT YOURSELF PROACTIVELY
This particular attack is over, but the supply-chain-via-CDN mechanism is not going away. Here is what actually reduces your exposure on a running WordPress server right now.
Monitor administrator account creation. WordPress does not notify you by default when a new admin account appears. Fix that with the user_register hook — add the following to your functions.php or a mu-plugin. It fires on every new registration, checks the assigned role, and sends you an email if it is administrator. A new admin account is rare enough that every such event deserves immediate attention:
add_action('user_register', function($user_id) {
$user = get_userdata($user_id);
if (in_array('administrator', $user->roles)) {
wp_mail(
get_option('admin_email'),
'New administrator registered: ' . $user->user_login,
'Login: ' . $user->user_login . "
Email: " . $user->user_email
);
}
});
Filesystem integrity monitoring with AIDE records the state of your filesystem at a known-clean baseline and compares against it on every subsequent run. A backdoor plugin appearing in wp-content/plugins/ will be caught at the next check — even if it hides from the dashboard. Initialize the database once, then run a check daily via a systemd timer:
# Initialize the baseline — run this on a clean system
sudo aide --init
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Check the current state against the baseline
sudo aide --check
Two-factor authentication for all WordPress administrators would not have stopped this specific attack — the script used an already-active session. But it closes adjacent vectors: session hijacking, credential stuffing, phishing. Supply chain attacks rarely arrive alone; they are often paired with attempts to harvest credentials through other means.
Finally, audit your administrator list on a regular schedule. Running sudo -u www-data wp user list --role=administrator once a week and comparing it against what you expect takes thirty seconds. It is the simplest possible check and one of the most effective at catching unauthorized accounts before they cause real damage.
HOW TO PREVENT THIS IN THE FUTURE
This attack exposed a problem that is difficult to eliminate entirely but can be significantly contained. The first lesson: CDN keys with write access must not be stored on servers that run WordPress or any other CMS. A marketing site is not secured infrastructure. Secrets with production access do not belong there.
The second lesson: monitoring administrator account creation should be part of your alerting system. A new WordPress administrator is an event you should learn about immediately, not from a weekly digest. This can be configured through security plugins or directly via WordPress hooks with email notification.
The third lesson: Subresource Integrity (SRI) is a mechanism allowing the browser to verify a hash of an external script before executing it. If the <script> tag included an integrity attribute with the SHA-256 hash of the expected file, the browser would have refused to execute the tampered SDK. Unfortunately, most SaaS plugins do not implement SRI for their SDKs — precisely because they regularly update those files. But for critical external scripts, this mechanism is worth considering where applicable.
The fourth lesson: two-factor authentication for all WordPress administrators would not have stopped this particular attack (the script used an already-active session), but it reduces risk from credential compromise scenarios that frequently accompany supply chain attacks.
INDICATORS OF COMPROMISE
Rogue accounts:
developer_api1 / [email protected] (fixed)
dev_xxxxxx / [email protected] (randomized, 6 characters)
Backdoor plugins (name rotates, check the filesystem):
content-delivery-helper "Content Delivery Helper" v2.7.1
database-optimizer "Database Optimizer" v2.9.4
Web shell parameters:
?developer_api1_fm (web shell, executes system($_POST['cmd']))
developer_api1_eval (executes base64-decoded PHP)
C2 infrastructure:
tidio.cc (IP: 84.201.6.54, AS214036 Ultahost)
Paths: /cdn-cgi/p, /cdn-cgi/b, /cdn-cgi/l, /cdn-cgi/pe-p, /cdn-cgi/pe-b, /cdn-cgi/pe-l
Malware encryption key:
jX9kM2nP4qR6sT8v
Tampered CDN files (already cleaned, but useful for retrospective log analysis):
a.omappapi.com/app/js/api.min.js
a.opmnstr.com/app/js/api.min.js
a.optnmstr.com/app/js/api.min.js
a.trstplse.com/app/js/api.min.js
clientcdn.pushengage.com/sdks/pushengage-web-sdk.js
CONCLUSIONS
The OptinMonster attack is uncomfortable precisely because it does not leave you with a simple takeaway like “update your plugins” or “use strong passwords.” The sites that were hit were doing everything right. The problem was not with them — it was with the vendor, in a place where site owners have no access and no visibility.
For Awesome Motive, this is a textbook case of why production secrets cannot live on infrastructure with a lower security posture. A marketing site running WordPress, UpdraftPlus, and a CDN API key with write access was a ticking clock. It detonated through a third-party plugin vulnerability that someone found and deliberately targeted for exactly this purpose.
For you as a WordPress administrator, this is a reminder that your security perimeter is wider than you think. It includes every external script, every CDN, every SaaS vendor your site trusts inside a visitor’s browser. Monitoring admin account creation, filesystem integrity checks through AIDE, a WAF with signature-based rules — these are not overreach. They are the only things that will actually help when the problem arrives not from your code, but from someone else’s.
