VYPR
Medium severity6.5GHSA Advisory· Published Jun 4, 2026· Updated Jun 4, 2026

Shopware: Privilege escalation: non-admin user with user:create ACL can create admin accounts

CVE-2026-48010

Description

UserController::upsertUser() writes user data in SYSTEM_SCOPE and does not filter the admin field. A non-admin API user with user:create or user:update ACL permission can set admin: true on new or existing users, escalating to full admin access.

The

Problem

In src/Core/Framework/Api/Controller/UserController.php, line 210-234:

public function upsertUser(?string $userId, Request $request, Context $context, ResponseFactoryInterface $factory): Response
{
    $data = $request->request->all(); // raw request data, no field filtering
    // ...
    $events = $context->scope(Context::SYSTEM_SCOPE, fn (Context $context) =>
        $this->userRepository->upsert([$data], $context)
    );
}

SYSTEM_SCOPE bypasses AclWriteValidator entirely (line 52 of AclWriteValidator::preValidate() returns early for SYSTEM_SCOPE). The admin boolean field is accepted without restriction.

Compare with IntegrationController::upsertIntegration() in the same codebase, which correctly checks:

if ((!$source instanceof AdminApiSource)
    || (!$source->isAdmin()
    && isset($data['admin']))
) {
    throw new PermissionDeniedException();
}

UserController is missing this exact check.

Impact

Any API user with the low-privilege user:create permission can create accounts with full admin access, or with user:update can promote any existing user to admin. This is a direct privilege escalation.

Suggested

Fix

Add the same isAdmin() check from IntegrationController:

$source = $context->getSource();
if ((!$source instanceof AdminApiSource) || (!$source->isAdmin() && isset($data['admin']))) {
    throw new PermissionDeniedException();
}

Best regards, Keyvan Hardani

Affected products

1

Patches

1
94569f7e71e6

Merge pull request #210 from shopware/fix/oauth-fixed-verification-time-backport-66

https://github.com/shopware/shopwareMarcel KrämlMay 19, 2026Fixed in 6.6.10.18via release-tag
2 files changed · +20 2
  • src/Core/Framework/Api/OAuth/ClientRepository.php+12 1 modified
    @@ -15,6 +15,11 @@
     #[Package('framework')]
     class ClientRepository implements ClientRepositoryInterface
     {
    +    /**
    +     * Bcrypt hash for a static dummy secret used to equalize timing when no client is found.
    +     */
    +    private const DUMMY_CLIENT_SECRET_HASH = '$2y$12$PVcA5R6ri9kS.7FnFUBRIOLwqU//bCicx5RFxwecAAccbmZ7V7PKu';
    +
         /**
          * @internal
          */
    @@ -34,8 +39,11 @@ public function validateClient($clientIdentifier, $clientSecret, $grantType): bo
                 }
     
                 $values = $this->getByAccessKey($clientIdentifier);
    +
                 if (!$values) {
    -                return false;
    +                // Prevent client enumeration via timing attacks by always running password_verify().
    +                $values = ['secret_access_key' => self::DUMMY_CLIENT_SECRET_HASH];
    +                $clientSecret = 'invalid-secret-will-always-fail';
                 }
     
                 if (!password_verify($clientSecret, (string) $values['secret_access_key'])) {
    @@ -67,6 +75,9 @@ public function getClientEntity($clientIdentifier): ?ClientEntityInterface
             $values = $this->getByAccessKey($clientIdentifier);
     
             if (!$values) {
    +            // Prevent client enumeration via timing attacks by always running password_verify().
    +            password_verify('invalid-secret-will-always-fail', self::DUMMY_CLIENT_SECRET_HASH);
    +
                 return null;
             }
     
    
  • src/Core/Framework/Api/OAuth/UserRepository.php+8 1 modified
    @@ -13,6 +13,11 @@
     #[Package('framework')]
     class UserRepository implements UserRepositoryInterface
     {
    +    /**
    +     * Bcrypt hash for a static dummy password used to equalize timing when no user is found.
    +     */
    +    private const DUMMY_PASSWORD_HASH = '$2y$12$PVcA5R6ri9kS.7FnFUBRIOLwqU//bCicx5RFxwecAAccbmZ7V7PKu';
    +
         /**
          * @internal
          */
    @@ -42,7 +47,9 @@ public function getUserEntityByUserCredentials(
                 ->fetchAssociative();
     
             if (!$user) {
    -            return null;
    +            // Prevent user enumeration via timing attacks by always running password_verify().
    +            $user = ['password' => self::DUMMY_PASSWORD_HASH];
    +            $password = 'invalid-password-will-always-fail';
             }
     
             if (!password_verify($password, (string) $user['password'])) {
    

Vulnerability mechanics

Root cause

"The UserController::upsertUser function does not filter the admin field when writing user data in SYSTEM_SCOPE."

Attack vector

An attacker with low-privilege API user credentials and the `user:create` or `user:update` ACL permission can exploit this vulnerability. By sending a request to the `upsertUser` endpoint with `admin: true` in the payload, the attacker can either create a new user with administrative privileges or promote an existing user to administrator. This bypasses the `AclWriteValidator` because the operation occurs within `SYSTEM_SCOPE` [ref_id=1].

Affected code

The vulnerability lies within the `upsertUser` method in `src/Core/Framework/Api/Controller/UserController.php`, specifically between lines 210 and 234. This method processes raw request data without filtering the `admin` field and operates within `SYSTEM_SCOPE`, which bypasses the `AclWriteValidator` [ref_id=1].

What the fix does

The suggested fix is to implement the same access control check found in `IntegrationController::upsertIntegration()`. This involves retrieving the source context and checking if it is an `AdminApiSource` and if the user is an administrator. If the source is not an admin source and the `admin` field is present in the data, a `PermissionDeniedException` is thrown, preventing unauthorized privilege escalation [ref_id=2].

Preconditions

  • authAttacker must have API user credentials with `user:create` or `user:update` ACL permission.

Generated on Jun 4, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

1