StyleSmuggler: Magento’s GraphQL 0-day, and what a real fix has to look like
StyleSmuggler: Magento’s GraphQL 0-day, and what a real fix has to look like
7 September 2026. Still unpatched. Adobe has confirmed they are working on a fix, with no ETA.
On 5 September, Sansec published StyleSmuggler: an unauthenticated remote code execution hole in Magento Open Source and Adobe Commerce. Attacks had already started the day before. They reproduced the full chain on clean 2.4.7, 2.4.8 and 2.4.9. The first victim they saw was on 2.4.6-p15, with the July and August patches applied and security:patch-status clean. So no, bumping to 2.4.9 does not get you out of this.
Sansec summed it up in two sentences, and they are still the right two sentences:
“StyleSmuggler injects malicious code into Magento's template system. By using the
stylesproperties, it can evade existing safeguards.”
What we have been seeing in traffic lines up with that, and makes the styles part less abstract. The PHP is not sitting in a .php file. It is stuffed inside a .gif. That GIF is passed through GraphQL styles parameters. Magento then executes it when it sends the payment-failed email.
We are not going to reconstruct the request. Sansec has not published the assembled chain either, and there is no good reason to. The rest of this post is about how Magento lets that happen, what we have actually observed, and what you can do before Adobe ships something real.
How the chain works
Sansec described two stages: poison a file Magento can later read, then make Magento execute that file while rendering a failed-payment email. Nobody has to open the mail.
“Nobody needs to open the email because the malicious code runs while Magento renders it. The attack can also succeed when email delivery fails.” — Sansec
Turning off SMTP does not help. If Magento rendered the template, the code already ran.
The poison file can be a failure report under var/report/, or var/log/system.log. Sansec also saw an operator fail against session storage and, eight seconds later, succeed with a custom-options upload. That last path matches what we keep seeing: a GIF Magento stored as an image.
From there the GraphQL request smuggles that file into the template engine through styles. Magento’s email templates already have a styles channel (<!--@styles -->, template_styles, {{css}} / {{inlinecss}}). GraphQL is not supposed to use any of that. It still does, because Magento copies extra query parameters onto the request object and never asks whether GraphQL needed them.
The actual include is Magento shooting itself in the foot. Disrex had a fully patched store compromised 50 minutes after the first confirmed attack worldwide. Their write-up, without publishing the exploit, points at Magento’s dependency-injection scanners: classes that exist for bin/magento setup:di:compile and have no business running on a storefront request. Those scanners include / require_once a path. PHP include is not “read this file.” It is “run this file as PHP.” A GIF header does not stop that. PHP skips the GIF89a bytes the same way it skips leading HTML, then hits the open tag.
GraphQL is just the front door
/graphql is on by default, unauthenticated, and Magento still boots a full framework request for it. Luma and Hyvä shops usually never touch it. Headless and PWA shops cannot live without it.
Sansec’s request shape is enough:
POST /graphql?styles[....]=
That query string is not a GraphQL feature. GraphQL already has query, variables and operationName. Magento’s GraphQL controller reads the JSON body on POST, then the HTTP request object still exposes every query parameter to the rest of the application. The built-in validators only check the HTTP verb and Content-Type. There is no allow-list. styles is just sitting on the request.
Moving sessions to Redis or the database does not close this. Sansec:
“Moving sessions to Redis or the database does not stop the attack. One merchant reported an attempt that failed against session storage and, eight seconds later, a second attempt that succeeded by using a file uploaded through Magento's custom options instead.”
The file just has to exist somewhere Magento can later include. Custom-option GIFs are a convenient somewhere.
Why a GIF gets through
Magento already treats gif as a normal image: product gallery, custom options, the media allow-list. Admin even tells you to enter extensions like “png, jpg, gif.”
A PHP-in-GIF file starts with a valid GIF header, so file, browsers and Magento’s MIME checks are happy. Somewhere after that header sits a PHP open tag. Requesting pub/media/.../something.gif over HTTP still serves an image. That is not how this payload runs. Nginx will not execute a .gif. Magento’s PHP process will, if anything in the framework includes the path.
styles is how that path reaches the email renderer without looking like shell.php. Magento’s MaliciousCode filter looks for <?php in HTML-ish text, not inside a binary GIF. Scanners that only look for *.php miss it. GraphQL never inspects styles at all. And when the payment-failed mail goes out, Magento is doing what it always does: load styles, filter the template, inline CSS. The include happens in that pipeline.
If you suddenly get a burst of “Payment Transaction Failed Reminder” mails with no matching checkouts, that send is the detonation, not a side effect. Sansec noted the same thing: unexpected bursts are worth investigating, even though real declined payments generate the same template.
After they get in
The first payload Sansec saw was a small Rust backdoor. The process name has already been rotated, which is why a blog that only mentions one of them is already stale.
| First seen | Process name | Typical path |
|---|---|---|
| 4–5 Sep 2026 | [kworker/u:8:0] |
/tmp/.kw_*, ~/.local/share/.gvfsd/gvfsd-user |
| 6 Sep 2026 | fc-cache (v2.1.4) |
~/.cache/fontconfig/fc-cache, /tmp/.fc-*/fc-cache |
| 7 Sep 2026 | chronyd (v2.1.5) |
/tmp/.chrony-<8hex>/chronyd |
Sansec:
“When the attack succeeds, a backdoor background process is launched. This is a small Rust program that connects to the
99.84.67.186C2 server and waits for commands. So far, we have no indication that the backdoor has been weaponized.”
Later builds hide command-and-control in UDP/123 traffic that looks like NTP. Only the first four bytes are NTP. A process named chronyd sending nine NTPv4 server-mode datagrams every 60 seconds is not a Linux time client. Persistence is often written straight into the cron spool, so syslog never shows a REPLACE. The 2.1.5 build can also relaunch with no cron entry at all.
A genuine Linux [kworker] is owned by root and has no resident memory. A bracketed name under the web user, with real RSS, is not a kernel thread.
And this is not one group. On 7 September Sansec pulled a 485-byte PHP dropper off the same victim set, unrelated tooling, writing a web shell into the product image cache:
pub/media/catalog/product/cache/ss_<10hex>/sync_<10hex>.php
It 404s without a specific X-Cache-Token header. Killing the Rust process is not cleanup.
What you can actually do
If the shop does not need GraphQL, turn it off at the web server until Adobe ships a patch. That is Sansec’s advice for anyone not running Shield, and it is the cleanest option. Classic and Hyvä storefronts generally do not need the endpoint. Headless and PWA do.
location = /graphql {
return 403;
}
location ~ ^/graphql/ {
return 403;
}
If you do need GraphQL, do not turn the whole endpoint off. Block the two request shapes Sansec published: a GraphQL call that carries styles, and Magento’s PayPal transparent-redirect URL. Put this in the Magento server block before PHP.
# Prevent requests from abusing Magento 2 StyleSmuggler vulnerability
# Learn more here: https://sansec.io/research/stylesmuggler
location ~* /paypal/transparent/response/ {
return 403;
}
# Block "/graphql?styles[...]=", also when routed via index.php or a subdirectory
if ($request_uri ~* "/graphql.*styles") {
return 403;
}
Real GraphQL traffic does not need a styles query parameter. query, variables and operationName keep working. The PayPal location is fine if you are not on PayPal Payments Advanced / Payflow Transparent Redirect; if you are, test a declined payment on staging first.
This is a speed bump, not Adobe’s patch. $request_uri only sees the query string. PHP merges GET and POST, so the same keys in a JSON body walk around the if. Pair it with a Magento GraphQL request validator that rejects styles after PHP has merged the parameters, and with the unofficial CLI-only scanner patches from Disrex / ProxiBlue. Those patches stop the known include sink on web requests and leave setup:di:compile working. They do not clean a box that is already owned, and Disrex is honest that they block the current chain, not every possible one.
An upgrade, a GraphQL block, or a composer patch also does not remove an implant. If the shop was on the public internet from 4 September onward, look at processes, cron, Magento logs, and images under pub/media (especially custom options) before you congratulate yourself for deploying Nginx. Sansec’s indicators of compromise are the list to use; they are still changing.
What Adobe actually has to fix
Guarding today’s ?styles[ query string will last until someone moves the same data into the body. A vendor fix has to assume that.
GraphQL should only accept query, variables, operationName and the documented store/currency headers. {{block}} / {{layout}} should not be object factories reachable from untrusted input. DI scanners that include arbitrary paths should be unreachable from any web SAPI. Magento should not write attacker-controlled bytes that form a PHP open tag into reports, logs, sessions, or uploaded images. And rendering a payment-failed email should not be enough to execute a styles-referenced file.
Adobe’s next scheduled bulletin is 8 September. They confirmed on the 7th that they are working on a patch. Coverage of this bug is not confirmed. Until a verified fix is in, treat every current 2.4 line as vulnerable.
| Date (UTC) | Event |
|---|---|
| 2026-09-04 22:20 | First confirmed StyleSmuggler exploitation |
| 2026-09-04 22:40 | Sansec finds the campaign |
| 2026-09-04 23:10 | Implant flagged on unrelated stores; Disrex-managed shop compromised |
| 2026-09-05 | Sansec reproduces the chain on clean 2.4.7, 2.4.8 and 2.4.9 |
| 2026-09-05 07:15 | Sansec Shield starts blocking StyleSmuggler |
| 2026-09-05 | Sansec publishes the analysis |
| 2026-09-06 | Implant renames to fc-cache, version 2.1.4 |
| 2026-09-07 | Second attacker drops a PHP web shell; implant renames to chronyd 2.1.5; Adobe confirms they are working on a patch |
| 2026-09-08 | Adobe’s next scheduled security bulletin |
Credit for discovery, naming and first disclosure belongs to the Sansec Forensics Team. Disrex published the incident-response notes and the CLI-only scanner mitigation. The GIF-in-styles behaviour above is from live traffic we have seen on this campaign.