One styles Field, One Late Check: How CVE-2026-75650 Turns Magento's Payment Failure Email Into Unauthenticated RCE
Sansec called it StyleSmuggler, and once you see it running you understand why. The payload does not detonate on the request that plants it. It detonates hours later, from inside Magento itself, on a routine notification your store sends whenever a shopper's card gets declined.
We at BlueTeamAutomation keep noticing the same shape in the e-commerce breaches that reach our desks. The bug lives in a place nobody thinks of as attacker-reachable, the trigger is a mundane background job, and the log line that would give the game away is buried under everyday cron traffic. CVE-2026-75650, addressed by Adobe's out-of-band APSB26-146 on September 7, is the cleanest example of that shape we have seen in months. Adobe rates it CVSS 10.0. CISA added it to the KEV catalog the next day. Every version of Magento Open Source and Adobe Commerce from 2.4.4 through 2.4.9 is affected, which is to say, every supported version.
Six lines of GraphQL, one delayed shell
The exploit chain runs in two stages that meet at a familiar surface. Stage one is an unauthenticated POST to Magento's public GraphQL endpoint, carrying a styles input whose value contains Magento template directive syntax: {{block ...}}, {{template ...}}, or {{layout ...}}. Those directives are supposed to be filtered anywhere they cross a trust boundary, but on the styles field the sanitiser never fires, and the directive string is written into a Magento entity that later feeds an email template. Sansec's disclosure and the follow-up Tenable FAQ walk the field-level pass through in detail.
Stage two is the delayed detonation. Magento's async job queue eventually renders a "Payment Transaction Failed Reminder" email for the same store. The template engine walks the entity that carries the smuggled directive, resolves the injected {{block}} node against Magento's block factory, and hands the attacker's class name and constructor arguments to a gadget that behaves as if it were building an ordinary layout element.
Because the trigger is a legitimate scheduled email, defenders looking at web logs see nothing incriminating on the actual attack request, and the eventual PHP invocation shows up under the php-fpm worker that runs the mail cron. The initial HTTP request and the code execution live on different sides of a queue boundary, so timelining without cross-correlation is genuinely hard. The Hacker News summarised the two-stage flow shortly after the emergency patch dropped.
Object first, class check second
The heart of the bug is a small ordering mistake in one factory class. dor.ky's patch analysis lays it out cleanly: the pre-patch code path did the exact opposite of what you would want from a class name that came in over the wire.
The vulnerable factory received a class name string and a parameter array from the directive layer, called $this->objectManager->create($className, $args) immediately, and only then asked if ($obj instanceof BlockInterface). If the check failed the object was thrown away, but by that time the constructor had already run to completion. An attacker who chose their class carefully could put the useful side effect in the constructor itself, or in a callable set up inside the constructor argument array, and never care what the instanceof line thought of the result.
The specific gadget observed in the wild reaches a dependency-injection helper that includes a file path pulled from the same parameter array. Combine that with stage one, which writes attacker-chosen bytes into a file Magento itself controls (a var/log/system.log line, a var/report/ failure file, a session file, or a product custom-option upload path), and the include line does the rest.
APSB26-146 rewrites the check as is_a($className, BlockInterface::class, true) and puts it before the create() call, so a class that does not satisfy the interface is never built at all. Adobe shipped the same eight-file patch identically across every supported line. Atwix's release notes list the composer bundle as VULN-39341, with builds 2.4.4-p18, 2.4.5-p17, 2.4.6-p15, 2.4.7-p10, 2.4.8-p5, and 2.4.9.
Why this slips past standard e-commerce guardrails
Most Magento security stacks watch the front door: WAF signatures on /admin, integrity monitoring on public webroots, and file integrity monitoring on the CMS block table. StyleSmuggler ignores every one of those. The write lands in var/log, which is meant to be writable and is deliberately excluded from most integrity baselines. Execution happens under the same UID as php-fpm and looks, at the OS level, exactly like a scheduled email render, because that is what it is. Generic ModSecurity rules never look at the styles GraphQL argument, since it is expected to carry inline CSS, so a template directive traveling in that field skates past every ruleset we tested this week.
Two independent operators have been observed on the same compromised stores. The first drops the persistent Rust implant. A second, opportunistic actor comes back later and installs a PHP web shell in the product image cache directory, presumably by scanning for hosts that other attackers already own. BleepingComputer's coverage tracks the two-operator pattern.
The footprint on your host and in your logs
The Rust binary Sansec pulled off victim hosts is a multi-arch loader that runs on x86_64 and arm64. It installs at ~/.local/share/.gvfsd/gvfsd-user under the web server's home directory, renames its process to [kworker/u:8:0] so it blends into normal kernel worker output in ps and top, and beacons out over HTTPS on standard ports.
Concrete things to hunt for tonight:
- Any new file under
~/.local/share/.gvfsd/on Magento hosts, especially with an owner other than root. - Processes named
[kworker/u:8:0]whose parent isphp-fpmor a shell rather than[kthreadd]. Genuine kernel threads always have PID 2 as their parent, so any bracketed kernel-worker name attached to a userland parent is a masquerade. - Fresh PHP files under
pub/media/catalog/product/cache/or any other Magento media subdirectory. - Cron jobs,
systemdunits, oratentries created on any store that ran an affected version after September 4. - GraphQL POST bodies containing the substring
{{blockor{{templateinside astylesargument in the last 90 days of NGINX or Apache access logs.
Map the chain to MITRE ATT&CK as T1190 for initial access, T1546.004 for the job-queue-triggered execution, T1505.003 for the web shells, T1036.005 for the process masquerade, and T1053.003 for any cron-based persistence you find during the sweep.
Fix, hunt, and prove the patch actually took
Apply the composer bundle VULN-39341 that matches your line and roll every admin password, API key, integration token, and payment gateway secret on any store that ran an affected version between September 4 and your patch window, because assume every credential the app process touched is public now. Adobe's KB announcement walks the composer install step by step.
Verification is the piece defenders forget. Take the exact {{block class="..."}} payload from the Sansec IOC feed, replay it against a staging store on the patched build, and confirm the request returns a 400 with the sanitiser log line firing rather than the previous 200. A silent 200 is not proof the sanitiser rejected the payload; check var/log/system.log before you close the ticket. If your test payload still writes to var/log, the composer patch did not apply cleanly and the fix needs another pass.
At BlueTeamAutomation, this is exactly the loop we automate for our customers: BASzy replays the current attack payload against your staging build the moment new mechanism detail lands, EDR and SIEM correlate the delayed job-queue render back to the initial GraphQL request, and SOAR ticks the fixed inventory into your compliance evidence. Detection and proof, on the same clock.
Automate the detect-and-prove loop
BASzy validates that your defences actually catch the exploit chain, not just the CVE ID. See how continuous BAS with EDR, SIEM, and SOAR correlation closes the gap between advisory and evidence.
Explore BASzy →