No results found.

✓Adobe has shipped a patch — VULN-39341 (08/09/2026)

Apply Adobe’s patch — how. It supersedes the containment patch on this page; revert that first, never run both. Composer patches for 2.4.4-p18, 2.4.5-p17, 2.4.6-p15, 2.4.7-p10, 2.4.8-p5 and 2.4.9. A patch does not clean an infected node — the checks and recovery steps below still apply.

Last updated: 08/09/2026 — Adobe has shipped VULN-39341; it supersedes the containment patch here.

StyleSmuggler Zero-Day (CVE-2026-75650)

StyleSmuggler (CVE-2026-75650), tracked by Adobe as VULN-39341 and patched in bulletin APSB26-146, is an actively exploited, unauthenticated RCE affecting fully patched Magento Open Source and Adobe Commerce stores. The attack writes PHP into a Magento report or log file, then causes Magento to include it.

Start with the incident response overview. This page is an on-call runbook — checks, containment, recovery. The vulnerability analysis, implant internals and live indicators are Sansec’s, and their advisory is the source and the only thing updated in real time.

Check for compromise

Three implant builds so far, all the same implant renaming itself. The original runs as [kworker/u:8:0] from ~/.local/share/.gvfsd/; the 06/09 build runs as fc-cache from ~/.cache/fontconfig/ on a 13,43 * * * * cron; the 07/09 build runs as chronyd from /tmp/.chrony-<8hex>/.

✕An empty crontab no longer means clean (07/09/2026)

The chronyd build has no cron entry at all — it relaunches itself, with parent PID 1. Sansec attribute it to the same implant renaming rather than a fresh infection, because the 8-hex agent ID carried over from the earlier drop. Cron-based checks alone will now miss an infected host. The name is also chosen to defeat the obvious hunt: anyone filtering UDP/123 traffic will have whitelisted chronyd as the legitimate NTP daemon.

The commands below cover all three, and the IOC list has the paths and C2 hosts for each.

Run as the Magento filesystem user, then repeat the process and socket checks as root.

crontab -l | grep -iE 'gvfsd|fontconfig|fc-cache|chrony'
grep -rlE 'gvfsd|fc-cache|chronyd' /var/spool/cron/ 2>/dev/null
ls -la ~/.local/share/.gvfsd/ ~/.cache/fontconfig/fc-cache \
       /tmp/.kw_* /tmp/.cache_* /tmp/.gvfsd[-_]* /tmp/.fc[-_]* /tmp/fc-cache \
       /tmp/.chrony-*/chronyd 2>/dev/null
ps -eo user,pid,ppid,comm,args | awk '$1 != "root" && ($4 ~ /kworker|fc-cache|chronyd/)'

# free signal where the web user cannot write cron — the implant retries every minute
grep -r 'crontab command not allowed' /var/log/

# second, unrelated actor: PHP under pub/media, where there should be none
find pub/media -name '*.php'

~ must resolve to the PHP-FPM user’s home. The implant writes directly to the cron spool and uses both hyphenated and underscored paths. Remove its cron persistence before killing the process or it will recreate itself. A non-root kworker process is suspicious. fc-cache and chronyd are both names of real system binaries, so confirm with readlink /proc/<pid>/exe before acting on either; collect evidence before attaching a debugger, as the implant checks whether it is traced.

Where the web user cannot write a crontab the implant still tries once a minute, and every attempt is logged. Sansec report a merchant with www-data in /etc/cron.deny whose auth log carried thousands of these — the first line dated the infection more than a day before the process was noticed:

crontab[<pid>]: (www-data) AUTH (crontab command not allowed)

For each suspicious PID:

readlink -v /proc/<pid>/exe
readlink -v /proc/<pid>/cwd
sha256sum /proc/<pid>/exe
lsof -nP -a -p <pid>
ss -plantu

A failed exploit attempt immediately followed by a successful one, seconds apart from the same source, is a pattern worth alerting on — it is the operator falling back to a second write sink.

Search reports and logs from the Magento root:

grep -Ral --fixed-strings '<?' var/report/ var/log/
grep -Ral -e 'X_TRACE_' -e 'X-TRACE-' -e 'x_trace_' var/report/ var/log/
grep -Fal 'array_merge()' var/log/system.log

The array_merge() error indicates Magento included a poisoned file. Disrex found successful infections using var/log/system.log, so search both var/report and var/log.

Run eComscan against the entire account. The implant lives above the document root.

~/bin/ecomscan \
  --skip-dashboard \
  --format=json \
  --min-confidence=0 \
  --deep \
  /path/to/account/root

No matches or a clean scan do not prove the host is clean. Resolve scan errors before continuing.

⚠️Collect evidence before you kill anything

Drain the node and restrict outbound traffic first. Preserve /proc/<pid>/exe, process metadata, sockets, cron and relevant logs. Do not reboot or run composer install: both can destroy useful evidence.

Contain StyleSmuggler

1. Update Sansec Shield

Sansec Shield has a live virtual patch. Update it, then hunt every node for compromises that pre-date the rules.

2. Block GraphQL if you don’t use it

If you do not use GraphQL, block the complete route at every public ingress. Cover plain and index.php URLs, store prefixes and direct origin access. Stores using GraphQL must rely on Shield and application containment.

3. Apply Adobe’s patch (VULN-39341)

Adobe shipped VULN-39341 — CVE-2026-75650 — on 08/09/2026 as APSB26-146, with composer patches for 2.4.4-p18, 2.4.5-p17, 2.4.6-p15, 2.4.7-p10, 2.4.8-p5 and 2.4.9 — see Adobe’s announcement and download links. This is the fix to run. Everything below it is superseded and kept only for stores mid-rollout.

# revert any interim containment patch FIRST — it touches the same files
patch -p1 -R < stylesmuggler-containment.patch
patch -p1 --forward < VULN-39341_<your-version>.patch
bin/magento setup:di:compile     # BlockFactory gains a constructor argument
systemctl reload php8.5-fpm

One path stays open, unchanged by the patch: {{block class=X}} autoloads X before the type check rejects it, so a .php file written into generated/code or app/code still executes through the directive. It needs a separate file-write bug to reach. Graycore’s patch closes it with a {{block}} allowlist.

4. Interim containment patch (superseded)

Kept for stores mid-rollout. Revert it once Adobe’s patch is on.

These changes contain specific observed paths; they do not correct Magento’s template system or rule out alternative gadgets and writable data sources. They do not clean an infected host.

Closing one write sink does not close the attack. Moving sessions to Redis or the database is not a mitigation: Sansec record an attempt against session storage failing and the same operator succeeding eight seconds later through a file uploaded via Magento’s custom options. That fallback file is removed by Magento’s own cron, so its absence proves nothing. Treat the write side as uncontainable and expect the guards below to be the load-bearing half.

⚠️Provided as is

This is unofficial emergency containment for a live zero-day with no vendor fix. It is provided as is, with no warranty, and it will change as new variants and evidence surface — the gadget list is not closed and the observed request shapes have already mutated once. Test it on a staging environment against your own extension set before production, keep an eye on this page and Sansec’s advisory, and replace all of it with Adobe’s supported fix when one ships.

ℹ️Revision 2 supersedes revision 1

Revert revision 1 before applying revision 2. Never run both.

Revision 1 closed the one disclosed include sink. Revision 2 targets the cause both stages share — a caller-supplied class reaching a constructor before its type is checked — and keeps the sink hardening as well, so poisoned reports and logs stay inert either way.

Save the revision you deploy as stylesmuggler-containment.patch.

stylesmuggler-containment.patch
diff --git a/vendor/magento/framework/View/Element/BlockFactory.php b/vendor/magento/framework/View/Element/BlockFactory.php
--- a/vendor/magento/framework/View/Element/BlockFactory.php
+++ b/vendor/magento/framework/View/Element/BlockFactory.php
@@ -41,6 +41,18 @@
     public function createBlock($blockName, array $arguments = [])
     {
         $blockName = ltrim($blockName, '\\');
+        // Validate the resolved type before instantiating: the stock order runs an
+        // arbitrary caller-supplied constructor before the type check rejects it.
+        if (!is_string($blockName) || $blockName === '') {
+            throw new \LogicException('Block name must be a non-empty string');
+        }
+        $omConfig = \Magento\Framework\App\ObjectManager::getInstance()
+            ->get(\Magento\Framework\ObjectManager\ConfigInterface::class);
+        $instanceType = $omConfig->getInstanceType($omConfig->getPreference($blockName));
+        if (!is_subclass_of($instanceType, BlockInterface::class)) {
+            throw new \LogicException($blockName . ' does not implement BlockInterface');
+        }
+
         $block = $this->objectManager->create($blockName, $arguments);
         if (!$block instanceof BlockInterface) {
             throw new \LogicException($blockName . ' does not implement BlockInterface');
diff --git a/vendor/magento/module-backend/Model/Widget/Grid/Row/UrlGeneratorFactory.php b/vendor/magento/module-backend/Model/Widget/Grid/Row/UrlGeneratorFactory.php
--- a/vendor/magento/module-backend/Model/Widget/Grid/Row/UrlGeneratorFactory.php
+++ b/vendor/magento/module-backend/Model/Widget/Grid/Row/UrlGeneratorFactory.php
@@ -36,6 +36,18 @@
      */
     public function createUrlGenerator($generatorClassName, array $arguments = [])
     {
+        if (!is_string($generatorClassName) || $generatorClassName === '') {
+            throw new \InvalidArgumentException('Passed wrong parameters');
+        }
+        // Validate the resolved type before instantiating; the post-construction
+        // check below is kept so both invariants hold.
+        $omConfig = \Magento\Framework\App\ObjectManager::getInstance()
+            ->get(\Magento\Framework\ObjectManager\ConfigInterface::class);
+        $instanceType = $omConfig->getInstanceType($omConfig->getPreference($generatorClassName));
+        if (!is_subclass_of($instanceType, \Magento\Backend\Model\Widget\Grid\Row\GeneratorInterface::class)) {
+            throw new \InvalidArgumentException('Passed wrong parameters');
+        }
+
         $rowUrlGenerator = $this->_objectManager->create($generatorClassName, $arguments);
         if (false === $rowUrlGenerator instanceof \Magento\Backend\Model\Widget\Grid\Row\GeneratorInterface) {
             throw new \InvalidArgumentException('Passed wrong parameters');
diff --git a/vendor/magento/module-email/Model/Template/Filter.php b/vendor/magento/module-email/Model/Template/Filter.php
--- a/vendor/magento/module-email/Model/Template/Filter.php
+++ b/vendor/magento/module-email/Model/Template/Filter.php
@@ -408,6 +408,14 @@
     {
         $skipParams = ['class', 'id', 'output'];
         $blockParameters = $this->getParameters($construction[2]);
+
+        // Authored {{block}} params are always scalar; a non-scalar only arrives
+        // through $var substitution, which is how attacker constructor data gets in.
+        foreach ($blockParameters as $blockParameterValue) {
+            if ($blockParameterValue !== null && !is_scalar($blockParameterValue)) {
+                return '';
+            }
+        }

         $block = null;

diff --git a/vendor/magento/framework/Logger/Handler/Base.php b/vendor/magento/framework/Logger/Handler/Base.php
--- a/vendor/magento/framework/Logger/Handler/Base.php
+++ b/vendor/magento/framework/Logger/Handler/Base.php
@@ -95,6 +95,16 @@
             $this->filesystem->createDirectory($logDir);
         }

+        // StyleSmuggler containment: neutralise PHP open tags so a poisoned log
+        // file is inert whichever include gadget reaches it.
+        if (isset($record['formatted']) && is_string($record['formatted'])) {
+            $record['formatted'] = str_replace(
+                ['<?', '<%'],
+                ['&lt;?', '&lt;%'],
+                $record['formatted']
+            );
+        }
+
         parent::write($record);
     }

diff --git a/vendor/magento/framework/Webapi/ErrorProcessor.php b/vendor/magento/framework/Webapi/ErrorProcessor.php
--- a/vendor/magento/framework/Webapi/ErrorProcessor.php
+++ b/vendor/magento/framework/Webapi/ErrorProcessor.php
@@ -17,6 +17,7 @@
 use Magento\Framework\Exception\NoSuchEntityException;
 use Magento\Framework\Phrase;
 use Magento\Framework\Serialize\Serializer\Json;
+use Magento\Framework\Serialize\Serializer\JsonHexTag;
 use Magento\Framework\Webapi\Exception as WebapiException;

 /**
@@ -96,7 +97,7 @@
         $this->_logger = $logger;
         $this->_filesystem = $filesystem;
         $this->directoryWrite = $this->_filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);
-        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
+        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(JsonHexTag::class);
         $this->registerShutdownFunction();
     }

diff --git a/pub/errors/processor.php b/pub/errors/processor.php
--- a/pub/errors/processor.php
+++ b/pub/errors/processor.php
@@ -8,7 +8,7 @@
 namespace Magento\Framework\Error;

 use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot;
-use Magento\Framework\Serialize\Serializer\Json;
+use Magento\Framework\Serialize\Serializer\JsonHexTag;
 use Magento\Framework\Escaper;
 use Magento\Framework\App\ObjectManager;
 use Magento\Framework\App\Response\Http;
@@ -156,7 +156,7 @@
     /**
      * JSON serializer
      *
-     * @var Json
+     * @var JsonHexTag
      */
     private $serializer;

@@ -172,7 +172,7 @@

     /**
      * @param Http $response
-     * @param Json $serializer
+     * @param JsonHexTag $serializer
      * @param Escaper $escaper
      * @param DocumentRoot|null $documentRoot
      */
@@ -185,7 +185,7 @@
         $this->_response = $response;
         $this->_errorDir  = __DIR__ . '/';
         $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
-        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
+        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(JsonHexTag::class);
         $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
         $this->documentRoot = $documentRoot ?? ObjectManager::getInstance()->get(DocumentRoot::class);
         if (!empty($_SERVER['SCRIPT_NAME'])) {

Six changes.

  • BlockFactory::createBlock() — resolve preferences and virtual types, check BlockInterface before instantiating. Stock order instantiates first, so the constructor has already run by the time the check fails.
  • UrlGeneratorFactory::createUrlGenerator() — same bug, same fix.
  • Filter::blockDirective() — reject non-scalar {{block}} parameters. Removes the primitive rather than one gadget.
  • Report serializers — JsonHexTag on both writers, so new var/report files carry no raw PHP tags.
  • Logger\Handler\Base::write() — escape PHP open tags, so a poisoned var/log file is inert.

The last two are defence in depth, not the fix. The write side is not reliably containable.

Dropped from revision 1: Io::includeFile(), the three DI scanner guards and PaymentFailuresService::handle().

diff --git a/pub/errors/processor.php b/pub/errors/processor.php
--- a/pub/errors/processor.php
+++ b/pub/errors/processor.php
@@ -8,7 +8,7 @@
 namespace Magento\Framework\Error;

 use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot;
-use Magento\Framework\Serialize\Serializer\Json;
+use Magento\Framework\Serialize\Serializer\JsonHexTag;
 use Magento\Framework\Escaper;
 use Magento\Framework\App\ObjectManager;
 use Magento\Framework\App\Response\Http;
@@ -156,7 +156,7 @@
     /**
      * JSON serializer
      *
-     * @var Json
+     * @var JsonHexTag
      */
     private $serializer;

@@ -172,7 +172,7 @@

     /**
      * @param Http $response
-     * @param Json $serializer
+     * @param JsonHexTag $serializer
      * @param Escaper $escaper
      * @param DocumentRoot|null $documentRoot
      */
@@ -185,7 +185,7 @@
         $this->_response = $response;
         $this->_errorDir  = __DIR__ . '/';
         $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
-        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
+        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(JsonHexTag::class);
         $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
         $this->documentRoot = $documentRoot ?? ObjectManager::getInstance()->get(DocumentRoot::class);
         if (!empty($_SERVER['SCRIPT_NAME'])) {
diff --git a/setup/src/Magento/Setup/Module/Di/Code/Reader/ClassesScanner.php b/setup/src/Magento/Setup/Module/Di/Code/Reader/ClassesScanner.php
--- a/setup/src/Magento/Setup/Module/Di/Code/Reader/ClassesScanner.php
+++ b/setup/src/Magento/Setup/Module/Di/Code/Reader/ClassesScanner.php
@@ -132,6 +132,11 @@
     private function includeClass(string $className, string $fileItemPath): bool
     {
         if (!class_exists($className)) {
+            // phpcs:ignore Magento2.Security.Superglobal
+            if (PHP_SAPI !== 'cli' || isset($_SERVER['REQUEST_METHOD'])) {
+                throw new \RuntimeException('Class files can only be scanned from a command-line process.');
+            }
+
             // phpcs:ignore
             require_once $fileItemPath;
             return true;
diff --git a/setup/src/Magento/Setup/Module/Di/Code/Scanner/ArrayScanner.php b/setup/src/Magento/Setup/Module/Di/Code/Scanner/ArrayScanner.php
--- a/setup/src/Magento/Setup/Module/Di/Code/Scanner/ArrayScanner.php
+++ b/setup/src/Magento/Setup/Module/Di/Code/Scanner/ArrayScanner.php
@@ -15,6 +15,11 @@
      */
     public function collectEntities(array $files)
     {
+        // phpcs:ignore Magento2.Security.Superglobal
+        if (PHP_SAPI !== 'cli' || isset($_SERVER['REQUEST_METHOD'])) {
+            throw new \RuntimeException('DI array files can only be scanned from a command-line process.');
+        }
+
         $output = [];
         foreach ($files as $file) {
             if (file_exists($file)) {
diff --git a/setup/src/Magento/Setup/Module/Di/Code/Scanner/XmlInterceptorScanner.php b/setup/src/Magento/Setup/Module/Di/Code/Scanner/XmlInterceptorScanner.php
--- a/setup/src/Magento/Setup/Module/Di/Code/Scanner/XmlInterceptorScanner.php
+++ b/setup/src/Magento/Setup/Module/Di/Code/Scanner/XmlInterceptorScanner.php
@@ -99,6 +99,13 @@
             $className = preg_replace('/^([0-9A-Za-z]*)_([0-9A-Za-z]*)/', '\\1_\\2_controllers', $className);
             $filePath = stream_resolve_include_path(str_replace('_', '/', $className) . '.php');
             if (file_exists($filePath)) {
+                // phpcs:ignore Magento2.Security.Superglobal
+                if (PHP_SAPI !== 'cli' || isset($_SERVER['REQUEST_METHOD'])) {
+                    throw new \RuntimeException(
+                        'Controller class files can only be scanned from a command-line process.'
+                    );
+                }
+
                 require_once $filePath;
             }
         }
diff --git a/vendor/magento/framework/Code/Generator/Io.php b/vendor/magento/framework/Code/Generator/Io.php
--- a/vendor/magento/framework/Code/Generator/Io.php
+++ b/vendor/magento/framework/Code/Generator/Io.php
@@ -157,7 +157,23 @@
      */
     public function includeFile($fileName)
     {
-        return include $fileName;
+        $generationDirectory = $this->filesystemDriver->getRealPath($this->_generationDirectory);
+        $realFileName = $this->filesystemDriver->getRealPath($fileName);
+        $generationDirectory = $generationDirectory
+            ? rtrim(str_replace('\\', '/', $generationDirectory), '/') . '/'
+            : false;
+        $realFileName = $realFileName ? str_replace('\\', '/', $realFileName) : false;
+
+        if (!$generationDirectory || !$realFileName || strpos($realFileName, $generationDirectory) !== 0) {
+            throw new FileSystemException(
+                new \Magento\Framework\Phrase(
+                    'The generated code file "%1" is outside the generation directory.',
+                    [$fileName]
+                )
+            );
+        }
+
+        return include $realFileName;
     }

     /**
diff --git a/vendor/magento/framework/Logger/Handler/Base.php b/vendor/magento/framework/Logger/Handler/Base.php
--- a/vendor/magento/framework/Logger/Handler/Base.php
+++ b/vendor/magento/framework/Logger/Handler/Base.php
@@ -95,6 +95,17 @@
             $this->filesystem->createDirectory($logDir);
         }

+        // Temporary StyleSmuggler containment: neutralise PHP open tags on the
+        // fully formatted line, so a poisoned log file is inert whichever include
+        // gadget reaches it. Revert once Adobe ships a supported fix.
+        if (isset($record['formatted']) && is_string($record['formatted'])) {
+            $record['formatted'] = str_replace(
+                ['<?', '<%'],
+                ['&lt;?', '&lt;%'],
+                $record['formatted']
+            );
+        }
+
         parent::write($record);
     }

diff --git a/vendor/magento/framework/Webapi/ErrorProcessor.php b/vendor/magento/framework/Webapi/ErrorProcessor.php
--- a/vendor/magento/framework/Webapi/ErrorProcessor.php
+++ b/vendor/magento/framework/Webapi/ErrorProcessor.php
@@ -17,6 +17,7 @@
 use Magento\Framework\Exception\NoSuchEntityException;
 use Magento\Framework\Phrase;
 use Magento\Framework\Serialize\Serializer\Json;
+use Magento\Framework\Serialize\Serializer\JsonHexTag;
 use Magento\Framework\Webapi\Exception as WebapiException;

 /**
@@ -96,7 +97,7 @@
         $this->_logger = $logger;
         $this->_filesystem = $filesystem;
         $this->directoryWrite = $this->_filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);
-        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
+        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(JsonHexTag::class);
         $this->registerShutdownFunction();
     }

diff --git a/vendor/magento/module-sales/Model/Service/PaymentFailuresService.php b/vendor/magento/module-sales/Model/Service/PaymentFailuresService.php
--- a/vendor/magento/module-sales/Model/Service/PaymentFailuresService.php
+++ b/vendor/magento/module-sales/Model/Service/PaymentFailuresService.php
@@ -98,6 +98,12 @@
         string $message,
         string $checkoutType = 'onepage'
     ): PaymentFailuresInterface {
+        // Temporary StyleSmuggler containment.
+        $this->logger->warning('Payment failure email suppressed by temporary StyleSmuggler containment.');
+
+        return $this;
+
+        // phpcs:disable
         $this->inlineTranslation->suspend();
         $quote = $this->cartRepository->get($cartId);

Eight changes.

  • Report serializers use JsonHexTag, preventing new var/report and var/report/api files from containing raw PHP tags. The Web API change adapts Graycore’s mitigation.
  • Io::includeFile() rejects files outside the generated-code directory.
  • DI scanners refuse to include PHP during web requests. CLI deployment commands still work.
  • PaymentFailuresService::handle() returns before rendering. Failed-payment emails remain disabled until the patch is removed.
  • Logger\Handler\Base::write() escapes PHP open tags on the formatted line, so a poisoned var/log file is inert whichever include gadget reaches it. Covers system.log, debug.log, exception.log and cron.log in one place.

This revision closes the one disclosed include sink rather than the factories that reach it. It is superseded by revision 2; keep it only if you are already running it, or apply its Io.php, ClassesScanner.php, ArrayScanner.php and XmlInterceptorScanner.php hunks on top of revision 2 as defence in depth.

✓What has been tested, and what has not

Every change in this revision was applied to a clean Magento 2.4.7-p10 install and regression-tested. Be clear on the scope of the security testing, though.

Proven both ways — stage one. The log-poisoning primitive reproduces on an unpatched 2.4.7-p10 in a single unauthenticated request, and the patch stops it. Raw <?php reaching var/log went from present to zero across four poisoning routes: the GraphQL Store header, ___store on a REST route, /paypal/transparent/response/, and a PHP-tagged User-Agent to /customer/section/load/. No raw <? in var/report either.

Not proven — stage two. I could not reproduce end-to-end code execution on 2.4.7-p10, so the gadget guards are reasoned mitigations, not empirically verified ones. Treat “no payload executed” in that testing as absence of evidence, since execution was never demonstrated on an unpatched install to compare against. This has since been superseded: stage two does reproduce, and the claim made here originally — that with_resolved appeared nowhere in 2.4.7-p10 — was wrong. It is at vendor/aws/aws-sdk-php/src/AwsClient.php:282. See revision 2 for what is now proven.

Still works after patching: setup:di:compile from a wiped generated/, storefront, category, cart, login, admin, GraphQL with a valid store, REST, cron:run, and a full cache:flush and reindex. All eight modified files lint clean.

Version coverage. All eight anchors verified against release source for 2.4.6 through 2.4.9, and the logger hunk tested against both Monolog generations Magento uses. The full install-and-exercise run was on 2.4.7-p10 only.

Paths in both revisions are Composer-layout, relative to the project root. For a core-layout checkout, rebase vendor/magento/framework to lib/internal/Magento/Framework, and vendor/magento/module-email, module-backend and module-sales to app/code/Magento/Email, app/code/Magento/Backend and app/code/Magento/Sales.

No constructor signatures change, so compiled DI stays valid: apply and clear opcache. No recompile, no static content deploy, no maintenance window.

patch -p1 --dry-run < stylesmuggler-containment.patch
patch -p1 < stylesmuggler-containment.patch
systemctl reload php8.5-fpm          # or: cachetool opcache:reset

Rollback, same:

patch -p1 -R < stylesmuggler-containment.patch
systemctl reload php8.5-fpm

5. Quarantine files created before the patch

The patch does not change existing reports or protect var/log. Preserve suspicious files and move them outside every path available to the web runtime.

6. Server-level hardening

Consider disabling PHP’s proc_open and mounting /tmp, /var/tmp and /dev/shm with noexec. Test first: some deployment tools and extensions shell out.

Deploy and verify

Revision 2 needs no DI rebuild. Apply it, clear opcache and return nodes one at a time. Verify:

  • if blocked, GraphQL unreachable on every public hostname, and the origin blocked or authenticated
  • both the normal and Web API report writers default to JsonHexTag, and new reports escape tag characters
  • admin grids that use rowUrl generators still render rows — Sales, Customers, Attribute Sets, Email Templates, Newsletter Queue, Integrations
  • a {{for}} loop in a custom email template still renders
  • a PHP-tagged Store header lands in var/log/system.log escaped as &lt;?php, not raw
  • transactional email rendering still works, including failed-payment mail, and {{block}} directives in CMS blocks, CMS pages and email templates still render — note that never having seen a failed-payment email means nothing, as the code runs while the email is rendered and succeeds even when delivery fails
  • no suspicious process, persistence file or report marker on any node

Remove containment only after an Adobe fix covering StyleSmuggler is deployed and verified on every node:

patch -p1 -R < stylesmuggler-containment.patch
systemctl reload php8.5-fpm

If a node is compromised

Assume the attacker read app/etc/env.php, the encryption key and every credential available to PHP-FPM. Do not disinfect in place.

  1. Preserve memory, disk and central logs.
  2. Rebuild from a known-good source revision and base image.
  3. Apply the mitigations above before accepting traffic.
  4. Rotate every secret readable by the compromised Unix account.
  5. Invalidate admin and customer sessions.
  6. Audit admin users, integrations, OAuth tokens, CMS content, email templates, database triggers, cron, queues and SSH keys.
  7. Hunt across every node, environment and shared home directory.

Checking the fleet at scale

Add the checks above to your healthcheck playbook and run them as the Magento filesystem user on every node. Treat scan errors as failures and roll out in waves so a hit can be investigated immediately. There is a ready-made Ansible task in the incident response post.

Current IOCs

Last reviewed 07/09/2026. The campaign is live and infrastructure is rotating, so treat Sansec’s advisory as the authoritative current source and re-check before every hunt.

From Sansec’s StyleSmuggler research:

247.cdnflare.xyz            # malware download
209.141.43.95               # malware download, http://209.141.43.95/files/ (FranTech AS53667)
99.84.67.186:443            # C2, WebSocket over TLS
windwsecurity.run:443       # TCP, remote shell
ntp.timesysnc.net:123       # UDP, NTP-shaped C2 traffic
time.microsft.run:123       # UDP, NTP-shaped C2 traffic
pool.microsft.studio:123    # UDP, NTP-shaped C2 traffic
ntp.timesync.to:123         # UDP, NTP-shaped C2 traffic, fc-cache build
ntp.synctime.to:123         # UDP, fallback
ntp.syncstime.to:123        # UDP, fallback
api4.ipify.org              # plain-HTTP public IP lookup before beaconing
ipv4.icanhazip.com          # plain-HTTP public IP lookup before beaconing
ipv4.ident.me               # plain-HTTP public IP lookup before beaconing
ipinfo.io                   # plain-HTTP public IP lookup before beaconing
88.216.72.181               # attacker source, seen at multiple victims
182.182.152.48              # attacker source
209.73.130.148              # attacker source, successful exploit
76.31.99.207                # attacker source, failed exploit
77.239.124.107              # attacker source, follow-up requests
5.181.86.133                # historic — no longer listed by Sansec, unconfirmed
185.157.160.251:123         # C2, A record for ntp.timesync.to and ntp.timesysnc.net (07/09)

sha256 e315687a1dfe61ef4a5a5642214db6d3b2b05d81391285eebc2af664641a26a7
sha256 4352cabaa451e5a894535fbcc4d46628701303322a13745cb5479d7d0534ae8e   # kworker-linux-x64, new build
sha256 d2fbf9eb75c495bfea48790d3b228fab0c15a282419c3d3f5e49294c4e1a3e82   # kworker-linux-arm64, new build

/tmp/.kw_<random><random>
/tmp/.cache_<random><random>       # drop-path variant
/tmp/.fc-<8hex>/fc-cache
/tmp/fc-cache                      # drop-path variant
/tmp/.fc_<8hex>.lock               # holds the implant PID
~/.local/share/.gvfsd/gvfsd-user
~/.local/share/.gvfsd/.gvfsd_<8hex>.lock
~/.cache/fontconfig/fc-cache
process [kworker/u:8:0]
process fc-cache
process chronyd                    # 07/09 build, no cron, parent PID 1
/tmp/.chrony-<8hex>/chronyd
implant version 2.1.5              # beacon field; 2.1.4 in the 06/09 build
crontab */5 * * * * exec <home>/.local/share/.gvfsd/gvfsd-user
crontab 13,43 * * * * <home>/.cache/fontconfig/fc-cache >/dev/null 2>&1

User-Agent: python-requests 2.15.0         # on ALL attacker exploit requests
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
                                           # implant only, for its plain-HTTP IP lookups

A second, unrelated attacker has been seen at the same victims, arriving through the same poisoning vector and dropping a PHP web shell that runs whatever arrives in a task POST parameter. Different operator, same way in:

sha256 d61217ca0bca83204302fa7b41935ce36f73764559c156d5c980f2fedddffb6e   # 485-byte dropper
pub/media/catalog/product/cache/ss_<10hex>/sync_<10hex>.php               # web shell
X-Cache-Token: fced27f6d57702565353ecc11722533b                           # required; 404 without it
457cfa2fb7p5.daf892t5qau4og8pi4cghbc6fhm1dim3u.oast.site                  # exec callback
ss5_457cfa2fb7                                                            # marker echoed outside the PHP tags

Disrex recovered two more, from disk and from the running process on the same host:

sha256 8334b434fa3fe9f59cebe9609b11e0b1fd19d10212c45c705adec1902a1d06ef   # on disk
sha256 251fabd50d7b18a8b5e1b3ef5d64e7198c17244778f6461fb1ab07f6169bf220   # in memory

Hash from a sample recovered during my own investigation, since adopted into Sansec’s published list:

sha256 b79dfdc1eed860e0b76c629d6adfce251db379b0b45a6d728d4ef483f7551420

Four hashes for one implant, two from the same machine. Hash matching is a bonus, not a detection strategy.

Do not rely on hashes or a single source IP. Look for the combination of process identity, executable path, persistence, outbound traffic, poisoned reports and the original web requests.

Sansec also lists requests involving /paypal/transparent/response/, /customer/section/load/ and /graphql with suspicious payloads. Correlate these with report/log content and process activity; the routes alone are legitimate traffic. Include UDP/123 and plain HTTP in outbound hunting, not only HTTPS. Recheck the live advisory as indicators change.

References