✕ Developing incident — no vendor patch (06/09/2026)

Sansec is still investigating StyleSmuggler. Adobe has published no advisory, CVE or workaround. Magento Open Source 2.4.7–2.4.9 is affected, with one confirmed victim running fully patched 2.4.6-p15. Treat Adobe Commerce and Cloud as affected until confirmed otherwise. Everything below is temporary containment.

TL;DR

StyleSmuggler is being actively exploited against fully patched Magento Open Source and Adobe Commerce stores. Attacker-controlled data is written into a Magento report or log file, then evaluated by a later request. Run Sansec Shield, block GraphQL if you don't use it, hunt for the gvfsd-user implant, apply the temporary patch, and rebuild anything that comes back dirty.

StyleSmuggler is an unauthenticated Magento RCE with no vendor patch. Being current does not protect you. This is the response plan I am running across my Magento fleets, based on Sansec's research, IOCs and Shield rules.

Emergency checklist

For every production environment:

  1. Get Sansec Shield in front of the store if it isn't already.
  2. Block public access to GraphQL if your storefront and integrations don't use it. Do this regardless of Shield.
  3. Check every application node for the fake kworker process, gvfsd-user implant and persistence files.
  4. Inspect existing var/report, var/report/api and var/log files before deleting or replacing anything.
  5. Deploy the temporary containment patch to clean nodes.
  6. Recycle PHP-FPM and OpCache, then verify from outside the origin.
  7. If any implant or persistence is found, isolate and rebuild that node. Removing the visible process is not recovery.

Have I Been Compromised?

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

crontab -l | grep -i gvfsd
grep -rl gvfsd /var/spool/cron/ 2>/dev/null
ls -la ~/.local/share/.gvfsd/ /tmp/.kw_* /tmp/.gvfsd[-_]* 2>/dev/null
ps -eo user,pid,ppid,comm,args | awk '$1 != "root" && $4 ~ /kworker/'
grep -Ral -e 'X_TRACE_' -e 'X-TRACE-' var/report/ var/log/
grep -Ral --fixed-strings '<?' var/report/ var/log/
grep -Fal 'array_merge()' var/log/system.log

Disrex found successful infections using var/log/system.log, so search both var/report and var/log. The commands match both marker spellings and both lock-file separators seen so far. The implant can also write directly to /var/spool/cron/.

The array_merge() error is the strongest application-level signal: it means Magento's DI scanner included a poisoned file. No matches do not prove a host is clean. Resolve unreadable paths and scan errors before continuing.

~/.local/share/.gvfsd/ is the PHP-FPM user's home, not root's. Check every account on shared hosting. Normal kworker processes are root-owned; one running as the Magento user is an IOC.

For each suspicious PID, capture:

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

On one affected node I found a non-root [kworker/u:8:0] resolving to ~/.local/share/.gvfsd/gvfsd-user, running from Magento's pub directory and connected to local Redis. The on-disk and in-memory hashes differed, so hash /proc/<pid>/exe as well as the file.

⚠️ Collect evidence before you kill anything

Drain the node and restrict outbound traffic. Preserve /proc/<pid>/exe, its hash, process metadata, sockets, cron and relevant logs. Capture memory if your incident process supports it, and protect the evidence as sensitive data.

Remove persistence before killing the process or it will recreate itself. Do not reboot or run composer install: both can destroy useful evidence. Rebuild the node from a trusted image.

How to mitigate StyleSmuggler

This attack needs several layers of containment.

1. Sansec Shield

Sansec Shield has a live virtual patch and is being updated as the campaign changes. One Disrex victim was breached before the rules landed, so Shield reduces the exposure window; it does not replace threat hunting.

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. Do not rely on payload signatures.

Stores using GraphQL must rely on Shield and the patch below. Blocking the route does not clean existing poisoned files or infected hosts.

3. Apply the temporary containment patch

The patch below makes seven production changes. It has no application-level GraphQL block and no broad Magento\Setup autoloader restriction, because both cause more collateral damage than they prevent.

Save this as stylesmuggler-hardening.patch:

diff --git a/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php
index 4d2295f..a0dac3d 100644
--- a/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php
+++ b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php
@@ -98,6 +98,15 @@ class PaymentFailuresService implements PaymentFailuresInterface
         string $message,
         string $checkoutType = 'onepage'
     ): PaymentFailuresInterface {
+        // Temporary StyleSmuggler containment: avoid the disclosed second-stage
+        // template rendering path. Revert once Adobe ships a supported fix.
+        $this->logger->warning(
+            'Payment failure email suppressed by temporary StyleSmuggler containment.'
+        );
+
+        return $this;
+
+        // phpcs:disable
         $this->inlineTranslation->suspend();
         $quote = $this->cartRepository->get($cartId);

diff --git a/lib/internal/Magento/Framework/Code/Generator/Io.php b/lib/internal/Magento/Framework/Code/Generator/Io.php
index a533ec8..3bbc788 100644
--- a/lib/internal/Magento/Framework/Code/Generator/Io.php
+++ b/lib/internal/Magento/Framework/Code/Generator/Io.php
@@ -153,11 +153,28 @@ class Io
      *
      * @param string $fileName
      * @return mixed
+     * @throws FileSystemException
      * @codeCoverageIgnore
      */
     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/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php b/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php
--- a/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php
+++ b/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php
@@ -18,7 +18,8 @@ use Magento\Framework\Exception\NoSuchEntityException;
 use Magento\Framework\Message\AbstractMessage;
 use Magento\Framework\Phrase;
 use Magento\Framework\Validator\Exception as ValidatorException;
 use Magento\Framework\Serialize\Serializer\Json;
+use Magento\Framework\Serialize\Serializer\JsonHexTag;
 use Magento\Framework\Webapi\Exception as WebapiException;

 /**
@@ -98,7 +99,9 @@ class ErrorProcessor
         $this->_appState = $appState;
         $this->_logger = $logger;
         $this->_filesystem = $filesystem;
         $this->directoryWrite = $this->_filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);
-        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
+        // Web API report mitigation adapted from Graycore_StyleSmugglerPatch:
+        // https://github.com/graycoreio/magento2-style-smuggler-patch/commit/321b3d6a394f24b2ab4318068785889aeaa4ce16
+        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(JsonHexTag::class);
         $this->registerShutdownFunction();
     }

diff --git a/pub/errors/processor.php b/pub/errors/processor.php
index 8cb386e..425c15b 100644
--- a/pub/errors/processor.php
+++ b/pub/errors/processor.php
@@ -8,7 +8,7 @@ declare(strict_types=1);
 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 @@ class Processor
     /**
      * JSON serializer
      *
-     * @var Json
+     * @var JsonHexTag
      */
     private $serializer;

@@ -172,20 +172,20 @@ class Processor

     /**
      * @param Http $response
-     * @param Json $serializer
+     * @param JsonHexTag $serializer
      * @param Escaper $escaper
      * @param DocumentRoot|null $documentRoot
      */
     public function __construct(
         Http $response,
-        ?Json $serializer = null,
+        ?JsonHexTag $serializer = null,
         ?Escaper $escaper = null,
         ?DocumentRoot $documentRoot = null
     ) {
         $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
index b4aa770..015444e 100644
--- 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,15 @@ class ClassesScanner implements ClassesScannerInterface
     private function includeClass(string $className, string $fileItemPath): bool
     {
         if (!class_exists($className)) {
+            // Temporary StyleSmuggler containment: this requires arbitrary PHP from disk,
+            // and Magento Setup is a command-line concern. Refuse to run under a web SAPI.
+            // 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
index 51ca31d..64a8007 100644
--- 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,15 @@ class ArrayScanner implements ScannerInterface
      */
     public function collectEntities(array $files)
     {
+        // Temporary StyleSmuggler containment: DI array files include arbitrary PHP,
+        // and Magento Setup is a command-line concern. Refuse to run under a web SAPI.
+        // 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
index 880d77b..0a7261c 100644
--- 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,15 @@ class XmlInterceptorScanner implements ScannerInterface
             $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)) {
+                // Temporary StyleSmuggler containment: this requires arbitrary PHP from disk,
+                // and Magento Setup is a command-line concern. Refuse to run under a web SAPI.
+                // 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;
             }
         }

The patch targets the Magento source tree. For Composer projects, rebase the three app/code and lib/internal paths to their matching vendor/magento packages. pub/errors and setup come from magento2-base. Use your normal Composer patch tooling and do not force a rejected patch.

git apply --check stylesmuggler-hardening.patch
git apply stylesmuggler-hardening.patch

What it 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 remain available.
  • PaymentFailuresService::handle() returns before rendering. Failed-payment emails remain disabled while the patch is installed.

If your release process allows it, exclude the complete setup/ tree from web-runtime images.

4. Deal with reports and logs created before the patch

The serializer changes only protect new files. Search existing reports and logs before returning a node to service:

grep -Ral --fixed-strings '<?' var/report/ var/log/
grep -Ral --fixed-strings 'X_TRACE_' var/report/ var/log/

Preserve suspicious files with timestamps and hashes, then move them outside the web runtime. Rewriting them destroys evidence and does not make the host trustworthy.

Deploy and verify

Deploy a clean artifact to drained nodes, rebuild DI metadata, recycle PHP-FPM and return nodes one at a time. Verify the final artifact after Composer has deployed root files.

Verify that:

  • if you blocked it, GraphQL is unreachable through every public hostname, and the origin is blocked or authenticated too
  • both the normal and Web API error processors resolve JsonHexTag
  • newly generated files in var/report and var/report/api contain escaped tag characters rather than a raw PHP opening tag
  • bin/magento setup:di:compile succeeds and the storefront renders after generated/ is cleared
  • failed-payment handling logs the containment warning without rendering or sending the email
  • no suspicious process, persistence file or report marker exists on any fleet node

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

git apply -R stylesmuggler-hardening.patch

If a node is compromised

Do not spend the recovery window trying to disinfect it. Once code has run as the Magento filesystem user, assume it could read app/etc/env.php, database and Redis credentials, the Magento encryption key, payment/API credentials and any deployment secrets available to PHP-FPM.

My minimum recovery plan is:

  1. Preserve memory, disk and central logs.
  2. Build replacement nodes 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.

Run eComscan against the entire account rather than only the active release, because the implant lives above the Magento document root:

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

Capture volatile evidence first. Sansec notes that eComscan 1.9.7 may terminate the malicious process for Shield customers.

The scope argument matters more than it looks. Disrex had a store return a clean eComscan result while its cron persistence was actively re-adding itself, because the scan was pointed at the document root and the implant lives in the user's home directory. A clean scan at the default scope is not evidence of a clean host. Point it at the account root or the result means very little.

Unexpected bursts of failed-payment notification emails warrant investigation, although legitimate declines also generate them. Sansec reports that execution happens during rendering: nobody needs to open the email, and delivery failure does not prevent exploitation.

Current IOCs

ℹ️ IOCs last reviewed 06/09/2026

This list is a snapshot. The campaign is live and the operators are rotating infrastructure, so treat Sansec's advisory as the authoritative, current source and re-check it before every hunt.

Almost all of the following is from Sansec's StyleSmuggler research — they did the work here, I'm just repeating it so it's in one place with the response steps:

247.cdnflare.xyz            # malware download
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
88.216.72.181               # attacker source
5.181.86.133                # attacker source

sha256 e315687a1dfe61ef4a5a5642214db6d3b2b05d81391285eebc2af664641a26a7

/tmp/.kw_<random><random>
~/.local/share/.gvfsd/gvfsd-user
~/.local/share/.gvfsd/.gvfsd_<8hex>.lock
process [kworker/u:8:0]
crontab */5 * * * * exec <home>/.local/share/.gvfsd/gvfsd-user

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

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

The sample recovered during my own investigation had a different SHA-256 again, which is worth adding to your own hunting:

sha256 b79dfdc1eed860e0b76c629d6adfce251db379b0b45a6d728d4ef483f7551420

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

Two things people keep getting wrong with this list.

Hunt UDP/123, not just HTTPS. Three of the C2 endpoints speak NTP-shaped traffic on the standard NTP port. Egress rules that only inspect 443 will not see them, and neither will a hunt that greps web logs. If your nodes do not need to reach arbitrary time servers, pin them to your own NTP source and alert on everything else.

The request paths alone are not IOCs. Sansec observed the campaign using /graphql, /paypal/transparent/response/ and /customer/section/load/. All three are legitimate routes carrying ordinary traffic all day. They are only meaningful correlated with payload content, a poisoned report or log, and process activity in the same window.

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

Bonus Tip: How to check the fleet at scale?

Doing the checks above by hand is fine for a handful of stores, less so for a fleet. I've added the following to our standard healthcheck playbook so every node gets checked on the next run, and so the check sticks around long after the incident.

Run it as the Magento filesystem user, not root — ~ has to expand to the web user's home for the implant check to look in the right place.

Ansible Playbook

# tasks/healthchecks/security/stylesmuggler.yaml
---
- name: Look for the gvfsd implant and persistence files
  tags: stylesmuggler
  register: stylesmuggler_implant
  changed_when: false
  failed_when: false
  ansible.builtin.shell:
    executable: /bin/bash
    cmd: |
      set -eu
      for path in ~/.local/share/.gvfsd /tmp/.kw_* /tmp/.gvfsd[-_]*; do
        if [[ -e "$path" || -L "$path" ]]; then
          printf '%s\n' "$path"
        fi
      done
      if cron_output=$(LC_ALL=C crontab -l 2>&1); then
        printf '%s\n' "$cron_output" | awk 'tolower($0) ~ /gvfsd/'
      elif [[ "$cron_output" == "no crontab for "* ]]; then
        :
      else
        printf '%s\n' "$cron_output" >&2
        exit 2
      fi
      grep -rl gvfsd /var/spool/cron/ 2>/dev/null || true

- name: Look for a non-root kworker process
  tags: stylesmuggler
  register: stylesmuggler_kworker
  changed_when: false
  failed_when: false
  ansible.builtin.shell:
    executable: /bin/bash
    cmd: |
      set -o pipefail
      ps -eo user,pid,ppid,comm,args | awk '$1 != "root" && $4 ~ /kworker/'

- name: Look for poisoned reports and logs
  tags: stylesmuggler
  register: stylesmuggler_reports
  changed_when: false
  failed_when: false
  ansible.builtin.shell:
    executable: /bin/bash
    chdir: "{{ project_root }}"
    cmd: |
      grep -Ral -e '<?' -e 'X_TRACE_' -e 'X-TRACE-' var/report/ var/log/
      grep -Fal 'array_merge()' var/log/system.log

- name: Assert StyleSmuggler IOC status
  tags: stylesmuggler
  ansible.builtin.assert:
    that:
      - stylesmuggler_implant.rc == 0
      - stylesmuggler_kworker.rc == 0
      - stylesmuggler_reports.rc in [0, 1]
      - stylesmuggler_implant.stdout | trim == ''
      - stylesmuggler_kworker.stdout | trim == ''
      - stylesmuggler_reports.stdout | trim == ''
    fail_msg: >-
      A check failed or possible StyleSmuggler IOCs were found. Review each task's
      stderr and stdout. For suspected compromise, drain the node and collect evidence
      before killing anything. See https://www.samdjames.uk/docs/platforms/magento/security/stylesmuggler/
    success_msg: "No known indicators found by these checks; this does not establish that the host is clean."

- name: Look for report-serializer patch markers
  tags: stylesmuggler
  register: stylesmuggler_patch
  changed_when: false
  failed_when: false
  ansible.builtin.shell:
    executable: /bin/bash
    chdir: "{{ project_root }}"
    cmd: |
      set -e
      grep -q JsonHexTag pub/errors/processor.php
      if [[ -f vendor/magento/framework/Webapi/ErrorProcessor.php ]]; then
        grep -q JsonHexTag vendor/magento/framework/Webapi/ErrorProcessor.php
      else
        grep -q JsonHexTag lib/internal/Magento/Framework/Webapi/ErrorProcessor.php
      fi

- name: Assert report-serializer marker status
  tags: stylesmuggler
  ansible.builtin.assert:
    that:
      - stylesmuggler_patch.rc == 0
    fail_msg: "Report-serializer markers are missing or unreadable; review the deployed artifact."
    success_msg: "Serializer markers found; verify all four patch changes and runtime behaviour separately."

A few things worth knowing before you run it:

  • The /tmp/.gvfsd[-_]* glob and the two marker spellings are deliberate. The lock file uses an underscore where earlier write-ups showed a hyphen, and a later payload variant switched to a hyphenated marker. Matching one spelling misses live infections.
  • The array_merge() grep catches the TypeError Magento's DI array scanner raises when it includes a poisoned file. It is the one check here indicating the exploit reached its sink rather than merely being attempted.
  • The cron check reads /var/spool/cron/ as well as crontab -l, because the implant writes its entry straight into the spool directory.
  • failed_when: false lets the asserts distinguish no matches from scan failures using both exit status and output. Review stderr on any failed assertion; a failed scan is not a clean result.
  • Run the IOC tasks with --limit in waves if your fleet is large. A hit means you want to be looking at that node before the playbook has moved on to another fifty.
  • The serializer marker check is a quick drift check, not proof that the full patch is installed or effective. It does not verify the scanner guards or payment-failure suppression. Validate all seven changes and their runtime behaviour on the final artifact.

What these mitigations do not do

The disclosed attack uses two stages: attacker-controlled data is written into a Magento-controlled file, then that data is evaluated while Magento renders a failed-payment email. The controls above break the paths observed so far, but they are not a complete correction to Magento's template parser and cannot prove there is no alternative gadget or writable data source.

They also do not clean an infected node. WAF rules, an application patch, threat hunting and trusted rebuilds solve different parts of the incident. Use them together until Adobe's supported fix is available.

Going forward, how do we improve security posture?

The same takeaways from Session Reaper and PolyShell apply again, and StyleSmuggler adds one more.

1. Sansec Shield

Shield had a virtual patch out ahead of any vendor fix. Third time running now that it has been the control that mattered on day zero. It is still not a guarantee: stores were compromised before the rules landed, so pair it with the hunting and hardening below rather than treating it as cover.

2. Turn off what you don't use

If GraphQL had been off on the stores that don't use it, the disclosed entry point wouldn't have existed for them. The same goes for unused controllers, endpoints and modules — every one you disable is one you don't have to patch under pressure.

3. Bulk patching infrastructure

A temporary patch is only useful if you can push it across the fleet in minutes and roll it back just as fast when Adobe ships the real one. I've covered a few approaches with Ansible and a Magento module in magento2-patching.

4. Read only file system

Read-only application code reduces opportunities for persistence. Also restrict the PHP-FPM account's access to home directories, user cron and other writable locations, including temporary directories. Required writable paths still need review: protecting the application tree does not automatically protect the web user's home.

Nginx restrictions govern incoming HTTP requests. They do not prevent an already-running PHP process from including readable files, starting processes or establishing persistence after code execution.

Two smaller controls in the same spirit, both worth doing today because neither depends on knowing anything about this vulnerability. Add proc_open to PHP's disable_functions, which removes the mechanism the dropper used to spawn its process. Mount /tmp, /var/tmp and /dev/shm with noexec, which stops a payload staged there from executing at all. Test both in staging first: some deployment tooling and a handful of extensions do legitimately shell out.

5. Hunt above the document root

The implant lives in ~/.local/share, not in pub/. Scans scoped to the active release miss it entirely. Scan the whole account.

Conclusion

  • Run Sansec Shield, and block GraphQL outright if nothing you own needs it
  • Hunt for the non-root kworker process and gvfsd-user implant in the web user's home on every node, not just the ones showing symptoms
  • Collect volatile evidence before killing anything
  • Rebuild nodes with confirmed compromise and rotate every secret PHP-FPM could read; investigate report markers before concluding that execution occurred
  • Remove temporary containment only after a vendor fix explicitly covering StyleSmuggler has been deployed and verified on every node

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 in outbound hunting, not only HTTPS. Recheck the live advisory as indicators change.

References