Released: 2026-10-14
Upstream: Magento Open Source 2.4.9
Mage-OS 3.6.0 is a security release. It ports Adobe's October isolated patch 249-2026-10-001-CE (APSB26-153), whose headline issue is CVE-2026-103849 — an unauthenticated template injection in the email template engine that reaches remote code execution, scored CVSS 10.0. Alongside the port there are seven bug fixes and one bundled add-on feature update.
Every Mage-OS 3.x installation should upgrade.
composer require mage-os/product-community-edition 3.6.0 --no-update
composer updateThere is no single-package workaround for this release. The fix spans mage-os/framework as well as the Email, Customer, Persistent and Catalog modules, so upgrading the metapackage is the only supported route.
There are no dependency additions, removals or constraint changes in this release, extra.magento_version is unchanged at 2.4.9, and there are no changes to magento2-base or project-community-edition beyond version bumps.
Mage-OS 3.5.0 reaches end of life with this release.
Security
October isolated patch 249-2026-10-001-CE (APSB26-153)
Adobe shipped its October bulletin as an isolated patch against fixed version 2.4.9-2026-oct, so there is no new upstream Magento release — 3.6.0 still tracks Magento Open Source 2.4.9. Mage-OS has mapped the patch onto the source tree so you receive it through a normal Composer upgrade.
22 of the patch's files were ported, plus three unit test files updated for the behaviour changes (#365). See APSB26-153 for the full issue list, CVE identifiers and severity ratings.
Template injection to RCE — CVE-2026-103849 (CVSS 10.0)
Framework\Filter\Template replaces signature-based deferral of template directives with out-of-band deferral through a new Filter\Template\DeferredDirectiveRegistry. A directive a child template cannot resolve is handed to the parent as a registry placeholder rather than as text carrying an embedded signature, so a directive's own output can no longer be mistaken for a deferral instruction by a later pass. The $isSigned pass, the signing loop and embedSignatureIntoPattern() are removed.
Template content validation
- A new
Email\Model\Template\Validatorrejects template content that places a{{...}}directive inside an HTML attribute value or a CSSurl()argument that resolves to an absolute external target (http:,https:or protocol-relative//). Email\Model\AbstractTemplate::beforeSave()now runs that validator over both the content field and the styles field, replacing the narrower private regex that only coveredsrc,hrefandactionattributes in the content field.Email\Block\Adminhtml\Template\Previewvalidates the request-suppliedtextandstylesparameters before rendering a preview, and coerces both to strings, closing a stored XSS path through the admin preview.
Customer data exposure
- A new
Customer\Model\Plugin\CustomerRepository\RestrictedFieldsPluginrejectspassword_hash,rp_tokenandconfirmationas search-criteria filter fields or sort fields onCustomerRepositoryInterface::getList(), raisingInputException. It is registered for REST, SOAP and GraphQL. Customer\Model\ForgotPasswordToken\GetCustomerByTokenno longer looks a reset-password token up throughCustomerRepository::getList(); it filters a customer collection directly. The class is deprecated — reset tokens cannot be looked up this way any more.
Guest cart access
Magento\Persistent adds an AllowPersistentCustomerGuestCart plugin around Quote\Model\GuestCart\GetGuestCart::checkIsGuestCart(). A guest-cart endpoint may read a cart that still belongs to a persisted customer only when cart persistence is enabled, the customer is not logged in, the persistent session is active, and its customer id matches the cart's. Every other case falls through to the original guest-cart check.
Admin product mass attribute update
Catalog\Controller\Adminhtml\Product\Action\Attribute\Save::sanitizeProductAttributes() now builds a fresh array keyed by each attribute's canonical code instead of unsetting entries from the submitted array, so an unknown or differently-cased attribute code cannot survive into the saved set. The multiselect "Change" toggle is read under the canonical code.
Framework hardening
Framework\Escaper::escapeXssInUrl()runs translate-inline processing before decoding HTML entities to a fixed point rather than after, so inline-translation output is not re-decoded.Framework\Validator\EmailAddressrejects a quoted local part outright instead of passing it to the Laminas validator.Framework\Validator\HTML\ConfigurableWYSIWYGValidatoradditionally detects empty HTML comments (<!-->,<!--->), a nested<immediately followed by a tag name, and NUL bytes.<body>occurrences are now counted separately from the other patterns, so a single<body>no longer masks a malicious match found alongside it.Framework\View\Element\UiComponent\DataProvider\Sanitizernarrows its template-syntax detection to values containing${but not${{, rather than a broad/\$\{.+\}/smatch.Framework\Lock\Backend\FileLock::cleanupOldLocks()tests the lock file directly instead of re-deriving the lock name withbasename()and re-resolving it, fixing lock-file naming assumptions.
Root-level deferrals no longer leak the registry placeholder
A directive that defers at the root template has no parent left to resolve it. Re-running it through resolveDeferredDirectives() returns the same deterministic placeholder, which would then reach the output — so an internal token of the form :deferred:<hash>: could appear verbatim in rendered email content.
The directive is now emitted unchanged instead. Before out-of-band deferral the directive text survived in exactly this way, which is also what the existing integration case "Child template mode results in unmodified directive" in Email\Model\Template\FilterTest asserts, so this restores the previous observable behaviour as well as removing the leak. Covered by testRootLevelDeferralDoesNotLeakPlaceholder, which fails without the guard.
Notes for integrators who built on 3.5.0
3.5.0 added Mage-OS-specific hardening around this same attack surface. The new deferral mechanism reworks part of it, so a few things move for anyone who built on that release:
- Signature-based deferral is retired. The signing channel it relied on is gone, so there is no signed set left to narrow.
DirectiveOutputNeutralizeris retained, with itsneutralize()call kept inside the rewrittenresolveDirectives(). With nothing signing output it now always takes theencode()branch, which is stricter than before.deferToParent()remains public. 3.5.0 shipped it public so directive processors, not onlyTemplatesubclasses, can declare a deferral; narrowing it would break them.SignatureProvideris annotated as deprecated for deferral only, not blanket-deprecated, because it is still consumed byDirectiveOutputNeutralizer.
Bug fixes
- Sales PDF item rows no longer split across a page break. Five core item renderers passed a hardcoded
'shift' => 5, which made the block-level page-break check inAbstractPdf::drawLineBlocks()fire only in the last 20pt of a page; an item block that did not fit was started anyway and broken mid-block, putting the numeric columns of one item against the name fragment of another. The hardcoded shift is removed so the real block height is computed, and the check is guarded withfitsOnEmptyPage()so it cannot leave a page holding nothing but the item-table header. Extends ACP2E-4630, which had fixed the credit memo renderer only. (#304 — Tu Van) - Customer grid CSV export no longer fails for customers on the admin website.
Document::setWebsiteValue()built its website map fromStoreManager::getWebsites(), whose$withDefaultargument defaults to false and therefore omits the admin website (id 0). A customer created in the admin carrieswebsite_id0, so the export aborted with "Undefined array key 0" for the whole file rather than for one row. The map now requests the default website and falls back to an empty value for a missing or null website id, andsetConfirmationValue()coalesces on null instead of?:, since the admin website's id 0 is falsy. (#344 — Tu Van) - PayflowPro no longer reports a payment failure for a cart with no Payflow payment.
PayflowProResponsecalledPaymentFailuresInterface::handle()— which sends the merchant "Payment Transaction Failed" notification — on any validation failure of the supplied payload, without confirming the cart had ever entered a Payflow flow. The resolver now requirespayflowproorpayflowpro_cc_vaulton the cart; a genuine decline still notifies, and a cart with no Payflow method is refused with the existing "Transaction has been declined." error and no notification. Adds the module's first unit tests. (#341 — Tu Van) - Configurable products no longer serve a stale parent from cache on
addChild().LinkManagement::addChild()fetched the parent withforceReloaddefaulting to false, so in long-lived processes such as queue consumers it could operate on stale extension attributes — most visibly reporting "The parent product doesn't have configurable product options" when the database said otherwise. (#269 — Allan Fernandes) - SalesRule
Subselectno longer matches on a premature partial cumulative total.validate()checked its range condition against the running cumulative total after every cart item instead of the final summed total, so two non-overlapping quantity-range conditions could each match their own partial-sum snapshot when the qualifying quantity was split across more than one line item — for example 2+2 units across two products matching both a "1–3" and a "4–5" condition. The early return is now gated to the multi-shipping branch, which re-evaluates conditions per item by design; the standard path checks only the fully-summed total. Regression coverage includes non-monotonic operators (<,<=,==), not just range ties. (#350 — David Lambauer) - Async sales grid cursor no longer resets to 0, which could stop orders appearing in the sales grid. Backported from upstream ACP2E-4893. (#349 — Melinda Humphrey, upstream commit by Anna Bukatar)
- The interactive installer can configure OpenSearch HTTP authentication.
bin/magento installnever collected credentials, so a password-protected OpenSearch 2.12+ failed the connection test and installed with auth disabled. (#348 — Ajith, fixes #282)
Also: CONTRIBUTING.md now points new contributors at Mage-OS guides, DevDocs, Discord and the in-repo Code of Conduct instead of the Adobe CLA and Magento community links. (#345 — Ajith, fixes #305)
Bundled add-on updates
mage-os/module-rma2.4.1 → 2.5.0. The return period is now evaluated per item quantity from the shipment date rather than from the order date: unshipped quantities are always returnable (cancelled quantities and units refunded before shipment excluded), shipped quantities only while the shipment is within the configured number of days, and bundles shipped separately expire a parent unit only once all its components have expired.0still means no limit. Expired items are shown greyed out and unselectable in the customer, guest (Luma and Hyvä) and admin creation forms with the reason displayed, and the quantity check is enforced server side;OrderEligibility::getEligibleItems()now returnsis_eligibleanddisabled_reason. Return labels are reworded to right-of-withdrawal terminology, translated in the it, de, fr, es and ro i18n files. (mage-os/module-rma#53 — Samuele Martini)swissup/module-ignition1.3.2 → 1.3.4. Security and robustness fixes, including a newCensorSensitiveDatamodel, plus restored error handlers and the removal of anexitthat interfered with integration tests.elgentos/magento2-varnish-extendedstays at 2.0.7 and is now pinned explicitly in the release template. Version 2.1.0 adds a console command registered globally underCommandListInterfacewhose constructor eagerly resolvesCustomerRepositoryInterface, which queries EAV before the schema exists and so breakssetup:installon a fresh database.
All other bundled add-ons are unchanged from 3.5.0: module-automatic-translation 2.3.1, module-page-builder-template-import-export 1.9.0, module-admin-activity-log 2.0.2, module-meta-robots-tag 1.2.1, module-theme-optimization 2.3.0, module-page-builder-widget 1.5.0, module-inventory-reservations-grid 1.0.5, theme-adminhtml-m137 1.3.4, aligent/magento2-pci-4-compatibility 1.4.2, creatuity/magento2-interceptors 1.3.8 and element119/module-custom-admin-logo 2.0.0.
Upgrade notes
Mage-OS 3.6.0 tracks the same upstream release as 3.5.0 (Magento Open Source 2.4.9). No dependencies were added, removed or re-constrained.
Customer API search is restricted
Filtering or sorting a customerRepository search by password_hash, rp_token or confirmation now raises an InputException — "The search criteria filter field is invalid." or "The search criteria sort field is invalid." — across REST, SOAP and GraphQL. This is the intended fix, not a regression. Integrations that use any of these three fields in search criteria must be changed before you upgrade; nothing legitimate needs to read them.
If you depend on a different restricted field set, the list is DI-configurable on Magento\Customer\Model\Plugin\CustomerRepository\RestrictedFieldsPlugin.
Email template validation now covers the styles field
Saving a template whose content or styles places a {{...}} directive inside an HTML attribute value or a CSS url() that resolves to an absolute external target is rejected with "Template content validation failed. Please review template syntax." The styles field was not validated before, and the content-field rule was narrower — it only covered src, href and action. Review custom transactional templates that build URLs from directives, and preview them after upgrading. Admin preview applies the same validation to request-supplied content.
Template directive signing is gone
If you built on the signature-based deferral introduced in 3.5.0, that channel no longer exists: $isSigned, the signing loop and embedSignatureIntoPattern() are removed, and Framework\Filter\Template\SignatureProvider is deprecated for deferral. Deferral now goes through Framework\Filter\Template\DeferredDirectiveRegistry.
deferToParent() remains public, as it was in 3.5.0, so directive processors outside Template subclasses continue to work. DirectiveOutputNeutralizer is retained and now always encodes resolved directive output; it can still be disabled through DI if it interferes with a custom filter chain.
Guest cart endpoints and persistent sessions
A guest-cart endpoint can now read a cart owned by a persisted customer, but only under the full set of conditions listed above. If you previously relied on guest endpoints rejecting such carts outright, or on them accepting them unconditionally, test your checkout flow with cart persistence enabled.
Stricter validators
- Email addresses with a quoted local part (
"user name"@example.com) are now rejected as invalid format. These are legal per RFC 5321 but rarely usable in practice; if you have stored customer records in this form, they will fail revalidation on save. - The WYSIWYG validator rejects empty HTML comments, a nested
<followed by a tag name, and NUL bytes, and no longer lets a single<body>tag mask other malicious matches in the same content. Content that previously saved may now be rejected.
RMA return periods and labels change for merchants and customers
module-rma 2.5.0 changes behaviour your customers will see:
- Return windows now start at shipment, not at order placement, per item quantity. Because the law starts the period at delivery, which Magento cannot know, the "Return Period (Days)" configuration comment now advises adding your expected delivery time to the configured value. Review that setting before upgrading — leaving it unchanged effectively extends every return window by your shipping time.
- Labels are reworded to right-of-withdrawal terminology: "Request Return" and the creation page title become "Withdraw from purchase", the submit button becomes "Confirm withdrawal", and the account menu link, history page title and back link become "Withdrawals and Returns". If you override these strings or link to them in help content, update accordingly.
Contributors
Thanks to everyone who contributed to this release:
Allan Fernandes, Ryan Hoerr, Melinda Humphrey, David Lambauer, Marcel Martinez, Samuele Martini, Tu Van and Ajith.
Upstream credit to Adobe for the security patch this release ports, and to Anna Bukatar for the ACP2E-4893 fix backported in #349.