shopware / shopware/shopware

Anchored `preg_match()` patterns across `src/` omit the PCRE `D` modifier

Open
#20,344 0 comments 0 reactions 0 assignees View on GitHub
component/core domain/framework
Dominant language
PHP
Stars
3.4k
Forks
1.2k
Avg merge
2d 23h
Merged PRs (30d)
433

Description

`src/` has 38 `preg_match()` calls whose pattern is written inline and ends with `$` before the closing delimiter. Two carry the `m` modifier, where a per-line `$` is the intent. None of the other 36 carries `D`, so in each of them `$` also matches before a final newline and the check accepts its subject plus one trailing `\n`.

36 is a floor, not a total. A pattern built from a constant or a variable does not appear in that count, and 105 of the 283 `preg_*` calls in `src/` are built that way. `Uuid::isValid()` is one of them, `preg_match('/' . self::VALID_PATTERN . '/', $id)`, and it is the instance with traced consequences, reported separately in #20342.

The codebase already has the correct form, also behind a constant: `Checkout/DocumentV2/Struct/RenderInput.php:20` declares `'/^[a-z0-9_]+$/D'` and applies it at line 33. That is the precedent to copy.

Not every one of the 36 is a defect. Some match strings the application itself produced, where a trailing newline cannot occur: `Framework/Api/ApiDefinition/Generator/StoreApiGenerator.php:728` matches a generated schema reference such as `#/components/schemas/ShippingMethod`, and `Storefront/Framework/Twig/TemplateDataExtension.php:104` parses the `_controller` request attribute the router sets. The list under "Sites to audit" is the subset that checks external or operator-supplied input, and that subset is the work this issue asks for.

Four sites in `src/` already handle it, each in a different way:

| Site | Why it is safe |
|---|---|
| `Storefront/Theme/Validator/SCSSValidator.php:207-211` | Compares the match back to the input (`$parsed[0] === $hexCode`). The character class excludes `\n` and `$` is zero-width, so the match and the input differ and it returns false |
| `Content/ImportExport/DataAbstractionLayer/Serializer/Field/FieldSerializer.php:241` | `trim()` before the check |
| `Storefront/Mcp/Tool/ThemeConfigTool.php:147` | `trim()` before the check |
| `Checkout/Order/SalesChannel/SetPaymentOrderRoute.php:77` | `ParameterBag::getAlnum()` strips non-alphanumerics |

### One site already disagrees with itself

`System/SalesChannel/File/SalesChannelFileRequestPathResolver.php:51`:

```php
if ($segment === '' || $segment === '.' || $segment === '..' || preg_match('/^[A-Za-z0-9._-]+$/', $segment) !== 1) {
```

The string comparison rejects the segment `..`. The allowlist regex accepts `"..\n"`, because the class matches `..` and `$` then matches before the newline. So a guard whose purpose is blocking `..` admits a value one byte away from it. `$fileName` reaches this from a query parameter (`Api/SalesChannelFileController.php:48`) and a request body (`:66`), both of which carry `\n` freely.

This does not traverse. Twig's `FilesystemLoader::validateName()` tests `'..' === $part` (`vendor/twig/twig/src/Loader/FilesystemLoader.php:268`), and no path resolver on POSIX or Win32 equates `..\n` with `..`, so the request returns 404 where 400 belongs on an ACL-gated admin route. Reported because the guard is one normalization step away from being wrong, not because it is currently exploitable.

### Sites to audit

- `Content/Media/File/SvgContentValidator.php:440` and `:521` (data-URI and DOCTYPE allowlists in the SVG sanitizer)
- `Checkout/Customer/Validation/Constraint/CustomerZipCodeValidator.php:69` (per-country pattern from the database, interpolated as `"/^{$pattern}$/"`; the value is persisted and later printed on documents)
- `System/SalesChannel/File/SalesChannelFileRequestPathResolver.php:31` and `:51`
- `Content/ProductExport/Validator/OpenAiProductExportValidator.php:169`, `:203`, `:234`
- `Content/ProductExport/Validator/GoogleProductExportValidator.php:226`, `:237`
- `System/SystemConfig/Service/ConfigurationService.php:51`
- `Framework/Migration/Command/CreateMigrationCommand.php:58`
- `Installer/Controller/ShopConfigurationController.php:84`
- `Storefront/Theme/Command/ThemeCreateCommand.php:70`
- `Storefront/Storybook/StorybookService.php:113`
- `Elasticsearch/Admin/AdminSearcher.php:245`, `Elasticsearch/Admin/Indexer/ProductAdminSearchIndexer.php:132`
- `Core/Test/PHPUnit/Extension/FeatureFlag/FeatureFlagExtension.php:44`

Moot, listed so nobody re-reports them: `Storefront/Theme/ThemeConfigValueAccessor.php:256` already has `\s` in its character class, so it admits a newline with or without `D`. Several patterns in `Core/DevOps/` run only in static analysis and Danger rules.

### Fix

Per site, one of: add `D`, use `\z` instead of `$`, compare the match back to the input, or normalize the subject before checking. Prefer the anchor fix; it states the intent.

An ECS or PHPStan rule flagging `preg_match` with a pattern ending `$` and no `D` would stop the idiom recurring. Worth deciding as part of this issue.

Contributor guide

Open the contributing guide

Research direction

Read the existing D-modifier precedent in Checkout/DocumentV2/Struct/RenderInput.php, then audit the listed preg_match() sites, starting with System/SalesChannel/File/SalesChannelFileRequestPathResolver.php and its controller entry points. Done means each external or operator-supplied input check uses an appropriate anchored form or documented safe alternative, with the proposed ECS or PHPStan rule decision recorded.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.