CVE-2026-31888: Shopware/platform Shopware Has User Enumeration Vulnerability

MODERATE
Advisory Identifier: GHSA-gqc5-xv7m-gcjq
CVSS 5.3
CWE-204
Published
Advisory Summary

CVE-2026-31888 is a MODERATE (CVSS 5.3) shopware has user enumeration vulnerability (CWE-204: Observable Response Discrepancy) affecting Shopware/platform libraries in the Packagist ecosystem. The issue involves shopware has user enumeration via distinct error codes on store api login endpoint. Remediation is available by upgrading affected packages to version 6.7.8.1, 6.6.10.14, 6.6.10.15.

CVE-2026-31888 at a Glance

CVE Identifier CVE-2026-31888
GHSA Identifier GHSA-gqc5-xv7m-gcjq
Severity MODERATE
CVSS v3.1 Base Score 5.3
CWE Classification CWE-204: Observable Response Discrepancy
Affected Product Shopware/platform
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 5.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-31888

To remediate this vulnerability, upgrade all affected Shopware/platform dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

shopware/platform fixed: 6.7.8.1
Upgrade shopware/platform to 6.7.8.1
shopware/platform fixed: 6.6.10.14
Upgrade shopware/platform to 6.6.10.14
shopware/core fixed: 6.7.8.1
Upgrade shopware/core to 6.7.8.1
shopware/core fixed: 6.6.10.15
Upgrade shopware/core to 6.6.10.15
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for shopware/platform

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-31888, inspect your project dependencies for Shopware/platform:

inspect dependencies for shopware/platform

Analyze the terminal output to locate any version of Shopware/platform in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 6.7.8.1), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-204: Observable Response Discrepancy, where the product provides different responses to incoming requests in a way that reveals internal state information to an unauthorized actor outside of the intended control sphere. In Shopware/platform, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary The Store API login endpoint (`POST /store-api/account/login`) returns different error codes depending on whether the submitted email address belongs to a registered customer (`CHECKOUT__CUSTOMER_AUTH_BAD_CREDENTIALS`) or is unknown (`CHECKOUT__CUSTOMER_NOT_FOUND`). The "not found" response also echoes the probed email address. This allows an unauthenticated attacker to enumerate valid customer accounts. The storefront login controller correctly unifies both error paths, but the Store API does not — indicating an inconsistent defense. ## CWE - **CWE-204**: Observable Response Discrepancy ## Description ### Distinct error codes leak account existence The login flow in `AccountService::getCustomerByLogin()` calls `getCustomerByEmail()` first, which throws `CustomerNotFoundException` if the email is not found. If the email IS found but the password is wrong, a separate `BadCredentialsException` is thrown: ```php // src/Core/Checkout/Customer/SalesChannel/AccountService.php:116-145 public function getCustomerByLogin(string $email, string $password, SalesChannelContext $context): CustomerEntity { if ($this->isPasswordTooLong($password)) { throw CustomerException::badCredentials(); } $customer = $this->getCustomerByEmail($email, $context); // ↑ Throws CustomerNotFoundException with CHECKOUT__CUSTOMER_NOT_FOUND if email unknown if ($customer->hasLegacyPassword()) { if (!$this->legacyPasswordVerifier->verify($password, $customer)) { throw CustomerException::badCredentials(); // ↑ Throws BadCredentialsException with CHECKOUT__CUSTOMER_AUTH_BAD_CREDENTIALS } // ... } if ($customer->getPassword() === null || !password_verify($password, $customer->getPassword())) { throw CustomerException::badCredentials(); // ↑ Same: CHECKOUT__CUSTOMER_AUTH_BAD_CREDENTIALS } // ... } ``` The two exception types produce clearly distinguishable API responses: **Email not registered:** ```json { "errors": [{ "status": "401", "code": "CHECKOUT__CUSTOMER_NOT_FOUND", "detail": "No matching customer for the email \"probe@example.com\" was found.", "meta": { "parameters": { "email": "probe@example.com" } } }] } ``` **Email registered, wrong password:** ```json { "errors": [{ "status": "401", "code": "CHECKOUT__CUSTOMER_AUTH_BAD_CREDENTIALS", "detail": "Invalid username and/or password." }] } ``` ### Storefront is protected — Store API is not The storefront login controller demonstrates that Shopware's developers are aware of this risk class. `AuthController::login()` catches both exceptions together and returns a generic error: ```php // src/Storefront/Controller/AuthController.php:203 } catch (BadCredentialsException|CustomerNotFoundException) { // Unified handling — no distinction exposed to the user } ``` The Store API `LoginRoute::login()` does NOT catch these exceptions. They propagate to the global `ErrorResponseFactory`, which serializes the distinct error codes into the JSON response: ```php // src/Core/Checkout/Customer/SalesChannel/LoginRoute.php:54-58 $token = $this->accountService->loginByCredentials( $email, (string) $data->get('password'), $context ); // No try/catch — exceptions propagate with distinct codes ``` This inconsistency confirms the Store API exposure is an oversight, not a design decision. ### Rate limiting is present but insufficient for enumeration The login route has rate limiting (LoginRoute.php:47-51) keyed on `strtolower($email) . '-' . $clientIp`. This slows bulk enumeration but does not prevent it because: 1. The attacker only needs **one request per email** to determine existence 2. The rate limit key includes the IP, so rotating IPs resets the counter 3. The rate limiter is designed to prevent brute-force password guessing, not single-probe enumeration ## Impact - **Customer email enumeration**: An attacker can confirm whether specific email addresses are registered as customers, enabling targeted attacks - **Phishing enablement**: Confirmed customer emails can be targeted with store-specific phishing campaigns (e.g., fake order confirmations, password reset lures) - **Credential stuffing optimization**: Attackers with breached credential databases can first filter for valid emails before attempting password guesses, improving efficiency against rate limits - **Privacy violation**: Confirms an individual's association with a specific store, which may be sensitive depending on the store's nature (e.g., medical supplies, adult products) - **Email reflection**: The `CHECKOUT__CUSTOMER_NOT_FOUND` response echoes the probed email in the `detail` and `meta.parameters.email` fields, which could be leveraged in reflected content attacks ## Recommended Remediation ### Option 1: Catch both exceptions in LoginRoute and throw a unified error (Preferred) Apply the same pattern already used in the storefront controller: ```php // src/Core/Checkout/Customer/SalesChannel/LoginRoute.php public function login(#[\SensitiveParameter] RequestDataBag $data, SalesChannelContext $context): ContextTokenResponse { EmailIdnConverter::encodeDataBag($data); $email = (string) $data->get('email', $data->get('username')); if ($this->requestStack->getMainRequest() !== null) { $cacheKey = strtolower($email) . '-' . $this->requestStack->getMainRequest()->getClientIp(); try { $this->rateLimiter->ensureAccepted(RateLimiter::LOGIN_ROUTE, $cacheKey); } catch (RateLimitExceededException $exception) { throw CustomerException::customerAuthThrottledException($exception->getWaitTime(), $exception); } } try { $token = $this->accountService->loginByCredentials( $email, (string) $data->get('password'), $context ); } catch (CustomerNotFoundException) { // Normalize to the same exception as bad credentials throw CustomerException::badCredentials(); } if (isset($cacheKey)) { $this->rateLimiter->reset(RateLimiter::LOGIN_ROUTE, $cacheKey); } return new ContextTokenResponse($token); } ``` This ensures both "not found" and "bad credentials" return the same `CHECKOUT__CUSTOMER_AUTH_BAD_CREDENTIALS` code and generic message. ### Option 2: Unify at the AccountService layer For defense in depth, change `AccountService::getCustomerByLogin()` to throw `BadCredentialsException` instead of letting `CustomerNotFoundException` propagate: ```php // src/Core/Checkout/Customer/SalesChannel/AccountService.php public function getCustomerByLogin(string $email, string $password, SalesChannelContext $context): CustomerEntity { if ($this->isPasswordTooLong($password)) { throw CustomerException::badCredentials(); } try { $customer = $this->getCustomerByEmail($email, $context); } catch (CustomerNotFoundException) { throw CustomerException::badCredentials(); } // ... rest of password verification } ``` This protects all callers of `getCustomerByLogin()` regardless of how they handle exceptions. Note: `getCustomerByEmail()` is also called independently (e.g., password recovery), so that method should continue to throw `CustomerNotFoundException` for internal use — the normalization should happen at the login boundary. ### Additional: Fix registration endpoint The registration endpoint (`POST /store-api/account/register`) also leaks email existence via `CUSTOMER_EMAIL_NOT_UNIQUE`. For complete remediation, consider returning a generic success response and sending a notification email to the existing address instead. ## Credit This vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).

CVE-2026-31888 Affected Shopware/platform Versions

4 component(s)
Fixed Versions:
6.7.8.16.6.10.146.6.10.15
Product / Package Ecosystem Affected Versions Fixed Version
shopware/platform
Packagist
≥ 6.7.0.0
< 6.7.8.1
6.7.8.1
shopware/platform
Packagist
< 6.6.10.14
6.6.10.14
shopware/core
Packagist
≥ 6.7.0.0
< 6.7.8.1
6.7.8.1
shopware/core
Packagist
< 6.6.10.15
6.6.10.15

References

Frequently Asked Questions

What is CVE-2026-31888?
CVE-2026-31888 is a security vulnerability in Shopware/platform involving Shopware has user enumeration via distinct error codes on Store API login endpoint under CWE-204: Observable Response Discrepancy.
Which Shopware/platform versions are affected?
Affected versions include releases prior to 6.7.8.1, 6.6.10.14, 6.6.10.15 across affected packages: shopware/platform, shopware/core.
What version fixes CVE-2026-31888?
Upgrading to 6.7.8.1, 6.6.10.14, 6.6.10.15 (or the latest patched release for your respective branch) resolves the vulnerability.
Is shopware/platform affected?
Yes, shopware/platform is affected in releases prior to 6.7.8.1, 6.6.10.14. Upgrade your dependency to 6.7.8.1, 6.6.10.14 or later.
Is shopware/core affected?
Yes, shopware/core is affected in releases prior to 6.7.8.1, 6.6.10.15. Upgrade your dependency to 6.7.8.1, 6.6.10.15 or later.
What is the CVSS score of CVE-2026-31888?
CVE-2026-31888 has a CVSS v3.1 base score of 5.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N.
What CWE is associated with CVE-2026-31888?
CVE-2026-31888 is classified under CWE-204: Observable Response Discrepancy (The product provides different responses to incoming requests in a way that reveals internal state information to an unauthorized actor outside of the intended control sphere.).
How can I check Packagist dependencies for CVE-2026-31888?
Run the dependency tree command: 'inspect dependencies for shopware/platform'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-gqc5-xv7m-gcjq",
  "modified" : "2026-03-11T21:37:32Z",
  "published" : "2026-03-11T19:23:49Z",
  "aliases" : [ "CVE-2026-31888" ],
  "summary" : "Shopware has user enumeration via distinct error codes on Store API login endpoint",
  "details" : "## Summary\n\nThe Store API login endpoint (`POST /store-api/account/login`) returns different error codes depending on whether the submitted email address belongs to a registered customer (`CHECKOUT__CUSTOMER_AUTH_BAD_CREDENTIALS`) or is unknown (`CHECKOUT__CUSTOMER_NOT_FOUND`). The \"not found\" response also echoes the probed email address. This allows an unauthenticated attacker to enumerate valid customer accounts. The storefront login controller correctly unifies both error paths, but the Store API does not — indicating an inconsistent defense.\n\n## CWE\n\n- **CWE-204**: Observable Response Discrepancy\n\n## Description\n\n### Distinct error codes leak account existence\n\nThe login flow in `AccountService::getCustomerByLogin()` calls `getCustomerByEmail()` first, which throws `CustomerNotFoundException` if the email is not found. If the email IS found but the password is wrong, a separate `BadCredentialsException` is thrown:\n\n```php\n// src/Core/Checkout/Customer/SalesChannel/AccountService.php:116-145\npublic function getCustomerByLogin(string $email, string $password, SalesChannelContext $context): CustomerEntity\n{\n    if ($this->isPasswordTooLong($password)) {\n        throw CustomerException::badCredentials();\n    }\n\n    $customer = $this->getCustomerByEmail($email, $context);\n    // ↑ Throws CustomerNotFoundException with CHECKOUT__CUSTOMER_NOT_FOUND if email unknown\n\n    if ($customer->hasLegacyPassword()) {\n        if (!$this->legacyPasswordVerifier->verify($password, $customer)) {\n            throw CustomerException::badCredentials();\n            // ↑ Throws BadCredentialsException with CHECKOUT__CUSTOMER_AUTH_BAD_CREDENTIALS\n        }\n        // ...\n    }\n\n    if ($customer->getPassword() === null\n        || !password_verify($password, $customer->getPassword())) {\n        throw CustomerException::badCredentials();\n        // ↑ Same: CHECKOUT__CUSTOMER_AUTH_BAD_CREDENTIALS\n    }\n    // ...\n}\n```\n\nThe two exception types produce clearly distinguishable API responses:\n\n**Email not registered:**\n```json\n{\n  \"errors\": [{\n    \"status\": \"401\",\n    \"code\": \"CHECKOUT__CUSTOMER_NOT_FOUND\",\n    \"detail\": \"No matching customer for the email \\\"probe@example.com\\\" was found.\",\n    \"meta\": { \"parameters\": { \"email\": \"probe@example.com\" } }\n  }]\n}\n```\n\n**Email registered, wrong password:**\n```json\n{\n  \"errors\": [{\n    \"status\": \"401\",\n    \"code\": \"CHECKOUT__CUSTOMER_AUTH_BAD_CREDENTIALS\",\n    \"detail\": \"Invalid username and/or password.\"\n  }]\n}\n```\n\n### Storefront is protected — Store API is not\n\nThe storefront login controller demonstrates that Shopware's developers are aware of this risk class. `AuthController::login()` catches both exceptions together and returns a generic error:\n\n```php\n// src/Storefront/Controller/AuthController.php:203\n} catch (BadCredentialsException|CustomerNotFoundException) {\n    // Unified handling — no distinction exposed to the user\n}\n```\n\nThe Store API `LoginRoute::login()` does NOT catch these exceptions. They propagate to the global `ErrorResponseFactory`, which serializes the distinct error codes into the JSON response:\n\n```php\n// src/Core/Checkout/Customer/SalesChannel/LoginRoute.php:54-58\n$token = $this->accountService->loginByCredentials(\n    $email,\n    (string) $data->get('password'),\n    $context\n);\n// No try/catch — exceptions propagate with distinct codes\n```\n\nThis inconsistency confirms the Store API exposure is an oversight, not a design decision.\n\n### Rate limiting is present but insufficient for enumeration\n\nThe login route has rate limiting (LoginRoute.php:47-51) keyed on `strtolower($email) . '-' . $clientIp`. This slows bulk enumeration but does not prevent it because:\n\n1. The attacker only needs **one request per email** to determine existence\n2. The rate limit key includes the IP, so rotating IPs resets the counter\n3. The rate limiter is designed to prevent brute-force password guessing, not single-probe enumeration\n\n## Impact\n\n- **Customer email enumeration**: An attacker can confirm whether specific email addresses are registered as customers, enabling targeted attacks\n- **Phishing enablement**: Confirmed customer emails can be targeted with store-specific phishing campaigns (e.g., fake order confirmations, password reset lures)\n- **Credential stuffing optimization**: Attackers with breached credential databases can first filter for valid emails before attempting password guesses, improving efficiency against rate limits\n- **Privacy violation**: Confirms an individual's association with a specific store, which may be sensitive depending on the store's nature (e.g., medical supplies, adult products)\n- **Email reflection**: The `CHECKOUT__CUSTOMER_NOT_FOUND` response echoes the probed email in the `detail` and `meta.parameters.email` fields, which could be leveraged in reflected content attacks\n\n## Recommended Remediation\n\n### Option 1: Catch both exceptions in LoginRoute and throw a unified error (Preferred)\n\nApply the same pattern already used in the storefront controller:\n\n```php\n// src/Core/Checkout/Customer/SalesChannel/LoginRoute.php\npublic function login(#[\\SensitiveParameter] RequestDataBag $data, SalesChannelContext $context): ContextTokenResponse\n{\n    EmailIdnConverter::encodeDataBag($data);\n    $email = (string) $data->get('email', $data->get('username'));\n\n    if ($this->requestStack->getMainRequest() !== null) {\n        $cacheKey = strtolower($email) . '-' . $this->requestStack->getMainRequest()->getClientIp();\n\n        try {\n            $this->rateLimiter->ensureAccepted(RateLimiter::LOGIN_ROUTE, $cacheKey);\n        } catch (RateLimitExceededException $exception) {\n            throw CustomerException::customerAuthThrottledException($exception->getWaitTime(), $exception);\n        }\n    }\n\n    try {\n        $token = $this->accountService->loginByCredentials(\n            $email,\n            (string) $data->get('password'),\n            $context\n        );\n    } catch (CustomerNotFoundException) {\n        // Normalize to the same exception as bad credentials\n        throw CustomerException::badCredentials();\n    }\n\n    if (isset($cacheKey)) {\n        $this->rateLimiter->reset(RateLimiter::LOGIN_ROUTE, $cacheKey);\n    }\n\n    return new ContextTokenResponse($token);\n}\n```\n\nThis ensures both \"not found\" and \"bad credentials\" return the same `CHECKOUT__CUSTOMER_AUTH_BAD_CREDENTIALS` code and generic message.\n\n### Option 2: Unify at the AccountService layer\n\nFor defense in depth, change `AccountService::getCustomerByLogin()` to throw `BadCredentialsException` instead of letting `CustomerNotFoundException` propagate:\n\n```php\n// src/Core/Checkout/Customer/SalesChannel/AccountService.php\npublic function getCustomerByLogin(string $email, string $password, SalesChannelContext $context): CustomerEntity\n{\n    if ($this->isPasswordTooLong($password)) {\n        throw CustomerException::badCredentials();\n    }\n\n    try {\n        $customer = $this->getCustomerByEmail($email, $context);\n    } catch (CustomerNotFoundException) {\n        throw CustomerException::badCredentials();\n    }\n\n    // ... rest of password verification\n}\n```\n\nThis protects all callers of `getCustomerByLogin()` regardless of how they handle exceptions. Note: `getCustomerByEmail()` is also called independently (e.g., password recovery), so that method should continue to throw `CustomerNotFoundException` for internal use — the normalization should happen at the login boundary.\n\n### Additional: Fix registration endpoint\n\nThe registration endpoint (`POST /store-api/account/register`) also leaks email existence via `CUSTOMER_EMAIL_NOT_UNIQUE`. For complete remediation, consider returning a generic success response and sending a notification email to the existing address instead.\n\n## Credit\n\nThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "shopware/platform"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "6.7.0.0"
      }, {
        "fixed" : "6.7.8.1"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "shopware/platform"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "6.6.10.14"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "shopware/core"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "6.7.0.0"
      }, {
        "fixed" : "6.7.8.1"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "shopware/core"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "6.6.10.15"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/shopware/shopware/security/advisories/GHSA-gqc5-xv7m-gcjq"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-31888"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/shopware/shopware"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-204" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-03-11T19:23:49Z",
    "nvd_published_at" : "2026-03-11T19:16:05Z"
  }
}