CVE-2026-31888 — shopware/platform
MEDIUMCVE-2026-31888 is a medium-severity (CVSS 5.3) CWE-204 vulnerability in shopware/platform. A fix is available for shopware/platform — see the affected versions and patch details below.
Shopware has user enumeration via distinct error codes on Store API login endpoint
Exploitation Status
No confirmed exploitation observed yet
- CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-31888.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
CVE-2026-31888 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 377,636 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
shopware/platform🐘shopware/platform🐘shopware/core🐘shopware/coreReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
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:
// 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:
{
"errors": [{
"status": "401",
"code": "CHECKOUT__CUSTOMER_NOT_FOUND",
"detail": "No matching customer for the email \"[email protected]\" was found.",
"meta": { "parameters": { "email": "[email protected]" } }
}]
}
Email registered, wrong password:
{
"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:
// 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:
// 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:
- The attacker only needs one request per email to determine existence
- The rate limit key includes the IP, so rotating IPs resets the counter
- 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_FOUNDresponse echoes the probed email in thedetailandmeta.parameters.emailfields, 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:
// 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:
// 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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | shopware/platform | ≥ 6.7.0.0&&< 6.7.8.1 | 6.7.8.1composer require shopware/platform:^6.7.8.1 |
| 🐘Packagist | shopware/platform | all versions | 6.6.10.14composer require shopware/platform:^6.6.10.14 |
| 🐘Packagist | shopware/core | ≥ 6.7.0.0&&< 6.7.8.1 | 6.7.8.1composer require shopware/core:^6.7.8.1 |
| 🐘Packagist | shopware/core | all versions | 6.6.10.15composer require shopware/core:^6.6.10.15 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for shopware/platform, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update shopware/platform to 6.7.8.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-31888 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-31888 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-31888. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-31888 in your dependencies?
O3 Security finds CVE-2026-31888 across Packagist dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.