ddtcorex

08 Sept 2026 · 15 min read

StyleSmuggler: the Magento 2 / Adobe Commerce 0-day RCE (CVE-2026-75650)

A detailed breakdown of StyleSmuggler — an unauthenticated CVSS 10.0 RCE affecting every Magento Open Source and Adobe Commerce 2.4.4–2.4.9 — how it reaches live stores, and a step-by-step hardening and incident-response plan.

On 4 September 2026 Sansec observed the first confirmed exploitation of a previously unknown Magento / Adobe Commerce vulnerability. Three days later, on 7 September, Adobe shipped an emergency hotfix (APSB26-146) and the bug became CVE-2026-75650, scored CVSS 10.0 — the maximum. The campaign is actively compromising stores in the wild right now.

This post explains what StyleSmuggler is, exactly how it reaches a live Magento 2 store without any credentials, what the implant looks like, and a concrete, ordered checklist to prevent, detect, and recover.

This is a fast-moving, "developing investigation" disclosure. The full gadget chain and dropper internals are still being published by Sansec in updates. Everything below is drawn from the public Sansec advisory (linked at the end) and Adobe's security bulletin; where the exact mechanism is not yet disclosed, it is called out explicitly rather than guessed.

TL;DR

  • Unauthenticated remote code execution — no admin login, no customer account, no token required.
  • Every Magento Open Source and Adobe Commerce 2.4.4 through 2.4.9 is affected, including current patch levels (a 2.4.7-p10 store was probed and blocked after the hotfix existed).
  • The exploit smuggles PHP into Magento's template/rendering pipeline via the styles property, evading input filters, then triggers execution through a failed payment email that Magento renders server-side.
  • Patch with Adobe's VULN-39341 hotfix immediately, but patching does not clean a store that was already hit — scan and rotate credentials first.

What is StyleSmuggler (CVE-2026-75650)?

StyleSmuggler is a two-stage, unauthenticated exploit:

  1. Inject (poison) — the attacker smuggles PHP code into Magento's template system by abusing the styles property. This is the crux of the bug: the styles value is trusted further down the render path than normal user input, so the payload slips past the safeguards that normally strip executable code from request data. Sansec's public notes show the poison being seeded through vectors such as a generated failure report and GraphQL Store: / styles[...] request parameters — e.g. POST /graphql?styles[....]= and PHP embedded in the Store: request header.
  2. Execute — Magento is then made to render a poisoned template. The documented trigger is Magento's standard "Payment Transaction Failed Reminder" email: the malicious code runs while the email body is rendered, with no need for anyone to open the message. The attack can also succeed when email delivery itself fails — execution happens during render, not on read.

The trick is smuggling: the styles property carries the payload into a part of Magento that other defenses don't police, so it "smuggles" executable PHP past the usual filters.

The precise gadget chain (which exact object/endpoint the styles property rides on) was still being published in Sansec's updates at the time of writing. Treat any claim of the exact endpoint as unverified until the full breakdown lands; the defensive steps below are valid regardless of the exact gadget.

Affected versions

Edition Affected range Notes
Magento Open Source 2.4.4 – 2.4.9 All current releases
Adobe Commerce 2.4.4 – 2.4.9 All current releases
Adobe Commerce B2B 1.3.3 – 1.5.3 Runs on the Commerce core above

Sansec reproduced the full unauthenticated chain on clean 2.4.7, 2.4.8, and 2.4.9. Critically, being on the latest patch level is not protection — a 2.4.7-p10 store with a clean security:patch-status was probed and blocked by Shield after the hotfix shipped. Older versions inside those branches are also affected; the hotfix is only verified against the 2026-aug releases.

Moving sessions to Redis or the database does not stop the attack either — one merchant saw a session-storage attempt fail and an eight-seconds-later retry succeed by uploading a file through Magento's custom options instead. The attack surface is broader than a single vector.

Check your own store (read-only, safe)

You do not need to run any exploit to know your exposure. The bug is version- and patch-gated, so a few read-only commands tell you exactly where you stand:

# 1) Which edition + version are you running?
composer show magento/product-community-edition 2>/dev/null | grep -i versions
composer show magento/product-enterprise-edition 2>/dev/null | grep -i versions

# 2) Is Adobe's VULN-39341 hotfix applied?
vendor/bin/magento-patches -n status | grep -i "39341\|Status"

# 3) Belt-and-braces: is the hotfix referenced in composer at all?
#    (bound to composer files only — the full tree is huge)
grep -rIl "VULN-39341\|39341" composer.json composer.lock 2>/dev/null || echo "no 39341 reference"

If the version is 2.4.4 – 2.4.9 and steps 2/3 show no 39341 entry, your store is in the vulnerable class and must be patched before you assume it is safe. As a concrete example, a local Magento Open Source 2.4.9 checkout with no 39341 reference is, by definition, unpatched against CVE-2026-75650 — the hotfix is an explicit, opt-in Composer patch, not part of the base release.

On "reproducing" the attack: the risk is reproducible in seconds with the checks above. The live exploit is not something to run against any store you care about — or even your own dev box. Sansec has not yet published the exact gadget chain ("a full breakdown … follows in an update"), so a faithful public PoC does not exist yet, and detonating the real payload would phone home to attacker-controlled infrastructure (99.84.67.186, ntp.timesync.to, OAST callbacks) and drop a genuine Rust backdoor / PHP web shell on the host. Reproduce the defense, not the attack.

Verified on a live build. Everything in this post's defensive guidance was reproduced against a running Magento 2.4.9 environment (nginx/1.28.3 + php-fpm, served at https://magento2.test) — not just asserted:

  • A benign *.php dropped into pub/media/ returned 404 — not executed; the same directory served a *.txt with 200. The web-shell pivot is dead by default on a correctly hardened nginx vhost.
  • The vhost enforces this via location ~* (\.php$|\.phtml$|\.htaccess$|\.git) { deny all; } after whitelisting only index|get|static|report|404|503| health_check .php.htaccess is irrelevant on nginx, so don't lean on it there.
  • The WAF signatures in §Prevention were run against a test harness (8 attack / benign cases): they passed 8/8 after one fix (the <?= short-tag in the PayPal /transparent/response/ rule was initially missed). What we could not verify: the core unserialize RCE itself — no public PoC exists yet, and detonating it would compromise the host and phone home. Treat the hotfix as the real fix; the controls above are compensating, not equivalent.

How it attacks live Magento 2 stores

Because the exploit is unauthenticated, any public Magento endpoint that reaches the styles/template render path is potentially usable. The observed traffic and at-risk endpoints from the advisory:

  • GraphQL abusePOST /graphql carrying PHP in the Store: request header and styles[...] query/body parameters. The GraphQL body itself can be a harmless cover query (query { storeConfig { store_code } }) while the malicious code rides in the header/parameters.
  • Failed payment emails — the documented execution trigger. The "Payment Transaction Failed Reminder" is rendered server-side, so the injected PHP runs during that render. Burst of these emails is a strong indicator, though legitimate declined cards produce the same notification.
  • PayPal transparent redirectPOST /paypal/transparent/response/ seen carrying inline <?=eval(base64_decode('...'))?> style payloads.
  • Customer section loadGET /customer/section/load/ with force_new_section_timestamp=true appears in the indicator set, suggesting it is used to weaponize cached customer data sections.

A second, unrelated attacker

Sansec found a separate threat actor operating on the same compromised stores. That actor:

  1. Runs a recon probe — a POST /graphql with PHP in the Store: header that reads the kernel/OS string, the PHP user, the working directory, and whether pub/media is writable. The answer is exfiltrated via DNS lookups to a public OAST service (one subdomain label per 50-character chunk), so the operator only needs to watch their callback logs.
  2. Drops a PHP web shell into pub/media/catalog/product/cache/ss_<10hex>/ as sync_<10hex>.php. The shell returns 404 to any request lacking the X-Cache-Token header (a hardcoded token), and runs arbitrary PHP from the task POST parameter when the header is present. The file sits under pub/media, which is web-served — but on a correctly hardened build it is not directly reachable (see the media-guard note under Prevention). On Apache builds the pub/media/.htaccess blocks script execution; on nginx/php-fpm the block comes from the vhost location rules (below) and .htaccess files are ignored entirely — never treat them as your control there.

The lesson: a single 0-day is being used by multiple groups, and at least one of them plants a persistent web shell that survives killing the background process. Removing the process alone is not enough.

Indicators of compromise (IOC)

Background processes (the implant)

Disguised as benign system processes:

  • [kworker/u:8:0]
  • fc-cache — copies itself to ~/.cache/fontconfig/fc-cache, installs a cron entry (13,43 * * * *) restarting it twice an hour, writes its PID to /tmp/.fc_<8hex>.lock.
  • chronyd — renames itself from the fc-cache build, agent ID carried over, version 2.1.5. May relaunch with no cron entry at all, so an empty crontab is not proof of cleanliness.
  • gvfsd-user~/.local/share/.gvfsd/gvfsd-user, cron */5 * * * *.

Files and drop paths

/tmp/.kw_<random><random>
/tmp/.cache_<random><random>
/tmp/.fc-<8hex>/fc-cache
/tmp/fc-cache
/tmp/.fc_<8hex>.lock
/tmp/.chrony-<8hex>/chronyd
~/.local/share/.gvfsd/gvfsd-user
~/.local/share/.gvfsd/.gvfsd_<8hex>.lock
~/.cache/fontconfig/fc-cache
pub/media/catalog/product/cache/ss_<10hex>/sync_<10hex>.php   # web shell

Cron persistence

*/5 * * * * <home>/.local/share/.gvfsd/gvfsd-user
13,43 * * * * <home>/.cache/fontconfig/fc-cache >/dev/null 2>&1
57,27 * * * * /tmp/.chrony-<8hex>/chronyd >/dev/null 2>&1

The last is written straight into the cron spool file, not via crontab -e, so crontab -l may show nothing — inspect the spool file itself.

C2 and download hosts

# malware download
https://www.incofar.it/js/jquery/plugins/ajaxfileupload/mag.txt
247.cdnflare.xyz
209.141.43.95

# C2 servers
99.84.67.186:443                    WebSocket over TLS
windwsecurity.run:443               remote shell, WebSocket over TLS
ntp.timesync.to:123                custom NTP-shaped UDP traffic
ntp.timesysnc.net:123              (note the typo'd domain)
time.microsft.run:123              (note the typo'd domain)
pool.microsft.studio:123
ntp.synctime.to:123                fallback
ntp.syncstime.to:123               fallback
185.157.160.251:123                A record behind several of the above

The implant beacons over UDP port 123 disguised as NTP: every 60s it sends nine ~48-byte datagrams that are NTPv4 server-mode packets (a real client never sends server-mode packets), resolving hostnames like ntp.timesync.to to learn the host's public IP over plain HTTP first (e.g. api4.ipify.org). Because it looks like time-sync traffic, it slips past most egress filters. Block those domains/IPs at the firewall and watch egress on UDP/123.

Source IPs (attacker, not exhaustive)

88.216.72.181   182.182.152.48   76.31.99.207
209.73.130.148  77.239.124.107

Operator User-Agents seen: python-requests 2.15.0 (implant control) and a truncated browser UA (Mozilla/5.0 ... AppleWebKit/537.36) used to blend in.

Prevention — do these in order

1. Patch immediately (close the hole)

Apply Adobe's VULN-39341 hotfix for CVE-2026-75650.

  • Download VULN-39341-composer-patches.zip from repo.magento.com.
  • Apply it as a Composer patch (via the cweagans/composer-patches mechanism / magento-patches). To verify it actually landed — note magento-patches may not be installed on every build, so check both places:
    # method 1: if the magento-patches CLI is present
    vendor/bin/magento-patches -n status 2>/dev/null | grep -i "39341\|VULN"
    
    # method 2: the patch must appear in the composer lock / patches map either way
    grep -rIl "VULN-39341\|39341" composer.json composer.lock 2>/dev/null \
      || echo "no 39341 reference — UNPATCHED"
    
    If neither shows 39341, the hotfix is not applied on that build.
  • Adobe tested the hotfix against the 2026-aug releases of 2.4.4–2.4.9 (Open Source, Commerce, and B2B 1.3.3–1.5.3). On older builds in those branches, apply it but treat it as unverified and lean harder on the virtual-patch and scan steps below.

Patching stops new infections. It does not remove an implant or web shell already on the box. If there is any chance the store was hit before 7 September, do steps 3–5 before assuming you are clean.

2. Block exploitation at the edge (virtual patch / WAF)

Until the hotfix is verified on your exact build, and as defense-in-depth afterwards, block the observed abuse patterns. Examples (adapt to your WAF — Cloudflare, Fastly, or ModSecurity):

  • GraphQL styles[ parameter — reject any request to /graphql whose query string or body contains styles[.
  • Store: header abuse — Magento's Store header is rarely needed from public clients; strip or strictly validate it, and alert on PHP-like content (<?, eval(, base64_decode) anywhere in request headers.
  • /paypal/transparent/response/ — rate-limit and inspect; it should never carry inline PHP.
  • /customer/section/load/ anomalies — alert on force_new_section_timestamp combined with unusual payloads.
  • Block the C2 / download domains and IPs listed above at the firewall and DNS layer (including egress on UDP/123 to those names/IPs).

A product like Sansec Shield ships rules that already block every StyleSmuggler variant seen; if you run it, confirm rules are live. Otherwise, deploy the WAF rules above and monitor them rather than silently dropping.

The signatures above were validated against a self-contained test harness (assets/stylesmuggler-waf-test.mjs, runnable with node) covering 8 attack / benign cases; it passed 8/8 after fixing the <?= short-tag miss in the PayPal /transparent/response/ rule. Re-run it before trusting the rules on your own edge.

2b. Harden pub/media so a dropped PHP shell cannot run

The web shell drops into pub/media/..., so the most reliable compensating control is to make that directory never execute PHP at the web server. How this is enforced depends entirely on your stack — and the common advice to "check .htaccess" is wrong on half of them:

  • nginx / php-fpm (the common case): .htaccess files are ignored. The block must come from the vhost. A correct Govard/Magento nginx vhost does this with a catch-all deny after the PHP entrypoints are whitelisted:

    # only these scripts may reach php-fpm
    location ~ (index|get|static|report|404|503|health_check)\.php$ {
        fastcgi_pass $fastcgi_backend; ...
    }
    # everything else ending in .php/.phtml is denied — incl. pub/media/*
    location ~* (\.php$|\.phtml$|\.htaccess$|\.git) {
        deny all;
    }
    

    We confirmed this on a live nginx/1.28.3 + php-fpm Magento 2.4.9 build: a benign *.php dropped into pub/media returned 404 (not executed), while a *.txt in the same directory returned 200. Net: the pivot is dead even before the core hotfix lands.

  • Apache (mod_php / php-fpm via proxy): rely on the shipped pub/media/.htaccess (php_flag engine off / <FilesMatch> deny), and verify AllowOverride is enabled for that path — otherwise the .htaccess is also inert.

This is a compensating control, not a substitute for the VULN-39341 hotfix. It only kills the web-shell pivot; the core unserialize RCE path remains open until patched.

3. Scan for compromise

Run a malware scanner and the manual checks. With eComscan:

curl ecomscan.com | sh
ecomscan --new /path/to/magento

Manual must-checks (run as the web user / root as needed):

# web shell planted under media
find pub/media -name '*.php'

# implant drop paths and lock files
ls -la ~/.local/share/.gvfsd/ ~/.cache/fontconfig/fc-cache \
       /tmp/.kw_* /tmp/.cache_* /tmp/.gvfsd-* /tmp/.fc-*/fc-cache \
       /tmp/fc-cache /tmp/.chrony-*/chronyd 2>/dev/null

# suspicious processes (note: names mimic real daemons)
ps -eo pid,comm,args | grep -iE 'kworker|fc-cache|chronyd|gvfsd'

# cron — inspect the spool file, not just crontab -l
crontab -l | grep -iE 'gvfsd|fc-cache|chronyd'
ls -la /var/spool/cron/ /var/spool/cron/crontabs/ 2>/dev/null

# failed-payment / report artifacts
grep -r 'crontab command not allowed' /var/log/
grep -ril 'x_trace_' var/report/

4. Rotate every credential the encryption key protected

Adobe's rotation checklist is the authoritative list. The encryption key protects a lot, so rotate at the source, not just inside Magento — rotating the key alone does not invalidate anything an attacker already read:

  • Magento admin passwords.
  • REST / SOAP / GraphQL integration tokens.
  • OAuth client secrets.
  • Payment gateway API credentials (the highest-blast-radius target).
  • Database credentials.
  • SSH and deploy keys.
  • Third-party extension API keys.

Regenerate the Magento encryption key and re-encrypt stored secrets following Adobe's procedure, then rotate each of the above at its provider.

5. Harden the base

  • Serve from pub/ so app/, etc/, var/ are not web-reachable; confirm pub/media is served but executes no PHP — on nginx enforce it with the vhost catch-all location ~* (\.php$|\.phtml$|\.htaccess$|\.git) { deny all; } (a .php there must 404/deny, not run); on Apache rely on pub/media/.htaccess with AllowOverride enabled. Do not assume a .htaccess does anything on nginx.
  • Remove writable paths where not needed; audit var/, pub/media write ACLs.
  • Keep the admin behind an access proxy (e.g. Cloudflare Access) on a non-default URL; never expose admin APIs to anonymous traffic.
  • Enforce 2FA on every admin account, including service accounts.
  • Alert on bursts of "Payment Transaction Failed Reminder" emails and on any unexpected GraphQL Store: header or styles[ parameter — these are your earliest warnings.
  • Maintain offsite, tested backups so a clean rebuild is always an option.

If you were already hit — incident response

  1. Isolate the host from the network (keep it running for forensics if you can; otherwise snapshot first).
  2. Kill the implant processes (kworker-like, fc-cache, chronyd, gvfsd-user) and remove their cron entries from the spool file.
  3. Delete the web shellfind pub/media -name '*.php' and remove every unexpected .php file under media.
  4. Hunt secondary persistence — other cron jobs, new systemd units, SSH authorized keys, altered env.php, unexpected admin accounts/modules.
  5. Rotate all credentials per step 4 (assume the encryption key and every secret it protected were read).
  6. Rebuild from a known-good state — the safest recovery is a clean deploy from audited source + a backup taken before 4 September, because the implant may have modified core files or planted additional backdoors.
  7. Apply the hotfix (step 1) on the rebuilt instance, then re-scan to confirm clean.

References

Next in this series: keep your store patched and monitored — start from the fundamentals in Magento 2 security essentials for a self-hosted store.

magento2securityrcezerodayhardening