Adobe shipped APSB26-92 on 11th August 2026, the second monthly isolated patch since the new cadence started properly. It’s already in m2-meta-security-patches as 2026.08.11.

Important note; isolated patches stack. August does not replace July. It is built against a codebase that already has July applied, and it applies on top of it. If you skipped APSB26-73, you need that first, then this one, in order. Last month’s post covers why they’re non-cumulative and what happens when you force one out of sequence.

This post is about what’s in the August drop, how to buy yourself time at the edge while you get it deployed, and the two packaging problems the July release exposed that I’d only half fixed at the time.

Am I affected?

Adobe lists 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. The meta-package currently ships the CE patches for the four supported lines:

  • 2.4.6-p15
  • 2.4.7-p10
  • 2.4.8-p5
  • 2.4.9

The 2.4.4/2.4.5 patches and the EE/B2B variants aren’t bundled. Grab those from Adobe’s download page if you need them.

What’s actually in it

Adobe’s bulletin describes it as arbitrary code execution, security feature bypass and privilege escalation. Reading the diffs, it’s four distinct things.

Customer account takeover. This is the one that matters. Magento\Customer\Controller\Account\Edit populated the customer data object from session form data with no allowlist at all. Every submitted key went through populateWithArray(), including id. Overwrite that and the session is holding a data object pointed at somebody else’s customer, which is a full account takeover from an ordinary logged in customer account. The patch filters the payload through the customer form’s allowed attributes and then re-pins the ID:

$data = array_intersect_key($data, $customerForm->getAllowedAttributes());
// ...
$customerDataObject->setId($customerId);

Both halves matter. The allowlist stops the arbitrary attributes, the setId() stops the ID overwrite specifically.

If you’re behind Sansec Shield, rules for this are already live, so you have cover while you schedule the deploy. If you’re not, see the WAF rules below.

Missing ACLs on the WYSIWYG media controllers. Five controllers under Magento\Cms\Controller\Adminhtml\Wysiwyg\Images (Upload, DeleteFiles, DeleteFolder, NewFolder, OnInsert) had no ADMIN_RESOURCE constant, so they fell back to the default and any authenticated admin user could upload or delete media regardless of role. The patch pins each to the matching Magento_MediaGalleryUiApi::* resource and adds a module sequence so those resources resolve.

Mass assignment on admin review save. Magento\Review\Controller\Adminhtml\Product\Save passed the raw request straight into $review->addData(). A submitted review_id would overwrite the loaded one, letting you save over a different review. Fixed with a one-line unset($data['review_id']).

Underscore.js bumped 1.13.6 to 1.13.8 in lib/web/underscore.js.

The two admin issues need an authenticated admin session first, so they’re escalation steps rather than entry points. The customer one needs nothing more than a registered account.

Buying time at the edge

Patch properly. But if you’re on a change freeze, or you’re managing a fleet and the rollout takes a few days (fix your process 😉), the customer account issue is cheap to block upstream. The payload reaches Account\Edit via the session, populated by a failed customer/account/editPost submission, so that POST is where you filter. createPost feeds the same session data, so cover both.

Sansec Shield’s rules for this are three conditions, and they’re a good template:

req.method equals POST
req.post   regex (?:^|&)id=
req.uri    contains customeraccounteditpost

Two things worth copying. First, only id is blocked, not a long list of parameters. id is the one that reassigns the data object to another customer, and every extra parameter you add is another chance to break a legitimate checkout flow. customer_group_id is worth adding on top, since group reassignment is the pricing and tax abuse case. Second, the URI is stripped to alphanumerics before matching, rather than compared as a raw string.

Cloudflare custom WAF rule. Needs a plan with request body inspection, so Business or above. http.request.body.form.names matches parameter names exactly, so no anchoring needed:

(http.request.method eq "POST"
  and any(http.request.body.form.names[*] in {"id" "customer_group_id"})
  and any(regex_replace(lower(http.request.uri.path), "[^a-z0-9]", "") contains {"customeraccounteditpost" "customeraccountcreatepost"}))

Stripping to alphanumerics is doing real work here. Doubled slashes, percent encoding, store code prefixes and Magento’s own route quirks all collapse to the same string, so you match the route rather than one spelling of it.

ModSecurity / Coraza. Pick an unused rule ID from your own reserved range rather than reusing the one below:

SecRule REQUEST_METHOD "@streq POST" \
    "id:900001,\
     phase:2,\
     deny,\
     status:403,\
     log,\
     msg:'APSB26-92: customer account mass assignment',\
     chain"
    SecRule REQUEST_URI "@rx customer[^a-z]*account[^a-z]*(edit|create)[^a-z]*post" \
        "t:urlDecodeUni,t:normalizePath,t:lowercase,chain"
        SecRule ARGS_POST_NAMES "@rx ^(id|customer_group_id)$" "t:lowercase"

HAProxy. Here you’re matching a raw body string, so anchor it properly:

http-request deny deny_status 403 if \
    METH_POST \
    { path,url_dec,lower,regsub([^a-z0-9]+,,g) -m sub customeraccounteditpost customeraccountcreatepost } \
    { req.body -m reg (?:^|&)(id|customer_group_id)= }

Two caveats before you paste any of this into production:

  • HAProxy’s req.body only sees the first buffer. Fine for a form POST of this size, not a general purpose body inspector.
  • These are defence in depth with a shelf life. They block the known payload shape, not the underlying flaw. Ship the patch.

What we got wrong in July

I wrote last month that the nginx.conf.sample hunk breaks fresh installs, and that the fix was to drop it. The diagnosis was right. The fix was lazy, and more to the point it didn’t scale: it only worked because that one hunk happened to touch a file nobody needs. The next patch that touches a magento2-base root file would have hit exactly the same wall.

The real problem is ordering. vaimo/composer-patches applies patches on PRE_AUTOLOAD_DUMP. magento/magento-composer-installer deploys magento/magento2-base’s root files on POST_INSTALL_CMD, which runs later. On a clean checkout with no vendor/, root files like nginx.conf.sample and lib/web/underscore.js don’t exist yet when patching runs. The hunk fails, the run halts, and it halts before the step that would have created the file. Every subsequent composer install hits the same state.

I also claimed that repointing the hunk at vendor/magento/magento2-base/ wouldn’t reliably work, because the deploy step won’t overwrite an existing root level file. Testing that properly, repointing is fine. The vendor copy always exists at patch time, and the deploy copies the patched version out to the project root.

So that’s now handled generically rather than per hunk: any hunk targeting a project root file owned by magento2-base gets repointed at the owning package path, in both the July and August patch sets. The nginx.conf.sample hunk I dropped from July is back in. If you’re on 2026.07.14-p1, upgrading picks it up, since the July patch files were rewritten in place rather than superseded.

The other one: vendor/bin/patch-status

Adobe’s isolated patches add vendor/bin/patch-status, the Commerce Version Tool I pointed at last month. It’s added as a new file hunk, which means it applies exactly once. Run composer patch:redo and it fails with:

vendor/bin/patch-status: already exists in working directory

That failure isn’t scoped to the patch containing the hunk. It takes down the whole redo run, every patch in it. Since it’s a version reporting CLI rather than a security fix, the meta-package now strips it. You lose vendor/bin/patch-status, you gain a patch:redo that works. If you want the tool, apply one CE patch from Adobe’s zip by hand, once.

Both changes are documented in the repo under Changes we make to Adobe’s patches. The patch files here are deliberately not byte identical to Adobe’s, and that should be visible rather than buried.

New: patches silently revert on reinstall

Reinstalling or updating magento/magento2-base re-extracts the package and reverts every patched file in it. vaimo/composer-patches doesn’t notice, because it records applied state against the patch file and never re-checks the target. So:

  • composer install prints Nothing to patch
  • composer patch:list still reports [APPLIED]
  • the files are unpatched

No warning anywhere. This isn’t specific to this meta-package, it affects any package patched through vaimo. Raised as vaimo/composer-patches#162 with a repro and two possible fixes.

Until it’s fixed, after anything that reinstalls magento2-base:

composer patch:redo

And verify, because patch:list will lie to you:

grep -m1 'Underscore.js 1.13' lib/web/underscore.js   # expect 1.13.8
grep -c customer_address nginx.conf.sample            # expect 1

Don’t wire patch:redo into a post-install-cmd hook. It triggers post-install-cmd itself and you get an infinite loop. Run it manually, or gate it behind a check in your deploy script.

Second vaimo issue this package has surfaced, after #157 on unrestricted patch sources. The plugin is doing a job Composer should arguably do natively, and the edges show.

Updating

If you track a range, it’s one command:

composer update samjuk/m2-meta-security-patches

If you pinned an exact version, bump it explicitly:

composer require samjuk/m2-meta-security-patches:">=2026.08.11"

Either way the version constraints on each patch mean nothing applies unless your installed module versions exactly match what the patch was built against, so it’s safe to run whether or not you’re on a line the August patch covers.

Don’t rely on Dependabot or Renovate for this one

I’ve recommended pairing this package with automated dependency tooling before, and for routine updates that still holds. For a security patch it doesn’t.

If your Dependabot or Renovate config is set up properly, it has a cooldown on new releases, deliberately, so you’re not the first to install a freshly compromised version. That’s the right default. It also means the PR for a security patch lands days after you needed it.

Dropping the cooldown for this package specifically would fix the timing and reintroduce exactly the risk the cooldown exists for. My own package is not a good reason to make that exception.

Treat the security bump as an explicit, triggered action instead of something you wait to receive:

  • a small script that runs composer update samjuk/m2-meta-security-patches and opens the PR itself, run by hand or from a workflow you dispatch
  • gh workflow run against a bump workflow, fanned across your fleet
  • if your Dependabot config is managed in Terraform, a targeted apply to force a re-run

The mechanism matters less than the property: you decide when it goes out, on patch day, rather than finding out when the bot gets round to it.

Supply chain note: vaimo/composer-patches lets any dependency declare patches by default. Restrict it to this package:

composer config --json "extra.patcher" '{"sources":{"packages":["samjuk/m2-meta-security-patches"]}}'

See restricting vaimo/composer-patches to trusted packages for the gotchas that make this silently no-op if you get it wrong.