Symfony Bundle Guide
The bundle registers a Regex service for your application and the
bin/console regex:* commands. Its configuration lives under the
php_regex key.
Requires PHP 8.2 or later and Symfony 7.4 or 8.x — 6.4 LTS is not supported.
The bundle ships no Symfony Flex recipe: register it by hand in
config/bundles.php.
Installation
composer require --dev php-regex/php-regex:2.x-dev// config/bundles.php
return [
// ...
PHPRegex\Symfony\PHPRegexBundle::class => ['dev' => true, 'test' => true],
];
Configuration
Every key is optional; the values below are the defaults.
# config/packages/php_regex.yaml
php_regex:
max_pattern_length: 100000
max_lookbehind_length: 255
# Whether the php_regex.regex service also compiles every pattern
# with the running PHP. regex:lint never does.
runtime_pcre_validation: false
# The PHP version and PCRE2 release regex:lint judges patterns for.
php_version: null # "8.2", "8.2.4" or 80200
pcre_version: null # "10.42"
cache:
pool: null # a PSR-6 pool service id, used before "directory"
directory: '%kernel.cache_dir%/php_regex'
prefix: regex_
extractor_service: null
redos:
enabled: false
threshold: high # low, medium, high or critical, in any case
ignored_patterns: []
analysis:
warning_threshold: 50
automata:
minimization_algorithm: hopcroft # hopcroft or moore
determinization_algorithm: subset-indexed # subset or subset-indexed
optimizations:
digits: true
word: true
ranges: true
canonicalize_char_classes: true
possessive: false
factorize: false
min_quantifier_count: 4
paths: [src]
exclude: [vendor]
ide: '%env(default::SYMFONY_IDE)%'
A value the bundle cannot read stops the container compile with the key
named: an unknown ReDoS threshold, a php_version that is no version, a
pcre_version that is no release. safe and unknown are not thresholds:
they are the verdicts a pattern gets.
redos.ignored_patterns lists patterns, fragments of patterns or whole
regexes the risk analysis skips, such as the requirement constants of
Symfony routes.
Using the service
The container registers php_regex.regex, autowirable as
PHPRegex\Toolkit\Regex — inject it like any other service:
use PHPRegex\Toolkit\Regex;
final class PatternSupport
{
public function __construct(private readonly Regex $regex) {}
public function check(string $pattern): bool
{
return $this->regex->validate($pattern)->isValid;
}
}
validate() returns the object the Laravel facade returns: $result->isValid, $result->error,
$result->caretSnippet. The API reference lists every
method of the service — parse, explain, redos, optimize, transpile
and the rest.
How route requirements and security patterns are read
Each one is read as the pattern Symfony runs:
- A route requirement is a fragment of the compiled route. Its leading
^or\Aand its trailing$or\zare stripped asRoute::sanitizeRequirement()does, and it is matched with{^...$}sD, plusuwhen the route sets itsutf8option:\d+is linted as{^\d+$}sD. A requirement that starts with/or#is no delimited regex there either. - A security
path(inaccess_controlor as a firewallpattern) is matched with{...}s, and ahostwith{...}i, without anchors:path: /apimatches/v1/apitoo.
The service and the lint judge for different targets
The php_regex.regex service (autowired as PHPRegex\Toolkit\Regex) runs in
your application: it judges patterns for the PHP running it, and
php_version / pcre_version do not change that.
regex:lint judges the patterns of your code for the PHP your project
supports. It picks the target in this order:
php_regex.php_versionandphp_regex.pcre_version;composer.jsonin%kernel.project_dir%:config.platform.phpif set, else the lowest versionrequire.phpallows, and then every pattern is also validated on the later PHP versions the constraint allows, as the standalone command does;- the PHP running the command.
Each version is chosen on its own: without pcre_version, the lint uses the
PCRE2 release the target PHP bundles (10.40 for PHP 8.2, 10.42 for 8.3, 10.44
for 8.4 and 8.5). A regex.json in the project is the configuration of the
standalone vendor/bin/regex command, not of the bundle.
The console report shows the target in its header. The JSON report carries it
as a top-level target object:
{
"target": {"php": "8.2", "pcre": "10.40", "source": "php_regex.php_version", "range": [{"php": "8.2", "pcre": "10.40"}]},
"stats": {"errors": 0, "warnings": 0, "optimizations": 0, "redos_errors": 0, "infos": 0, "lint_errors": 0, "parser_fallbacks": 0},
"results": []
}
source names where each version came from: php_regex.php_version,
composer.json require.php, composer.json config.platform.php or
running PHP, followed by php_regex.pcre_version when the PCRE2 release
came from there.
The report is the one vendor/bin/regex lint --format=json prints, and
regex:transpile --format=json prints the same document as
vendor/bin/regex transpile; the JSON output reference
lists every key.
runtime_pcre_validation compiles with the PHP that runs, which cannot tell
whether an older PHP accepts a pattern: the lint never uses it, whatever the
bundle says.
Commands
| Command | Description |
|---|---|
regex:lint |
Lint the regex patterns of your PHP code |
regex:compare |
Compare two patterns with automata |
regex:routes |
Detect route conflicts and overlaps |
regex:security |
Analyze access control ordering and firewall regexes |
regex:analyze |
Run the routes and security analyzers |
regex:transpile |
Translate a pattern for another regex engine |
bin/console regex:lint
bin/console regex:lint src/ --format=json
bin/console regex:analyze --redos-threshold=medium
--redos-threshold takes the same values as redos.threshold, in any case.
regex:lint reads the functions marked #[RegexPattern] (or PhpStorm’s
#[Language('RegExp')]) in the configured paths, with exclude where they are linted, and
in %kernel.project_dir%/vendor, whatever exclude says; a project
declaration wins over a copy in vendor/. A call to one is linted as a
preg_*() call (see the CLI guide).
ReDoS findings
With redos.enabled: true, regex:lint adds the ReDoS issue,
regex.lint.redos, to the lint findings of each pattern at or above
redos.threshold, as vendor/bin/regex lint --redos does (see
the CLI guide): the verdict’s headline, such as
Exponential backtracking (proven), then the severity, the confidence and the
attack.
regex:security and regex:analyze run the same analysis on the pattern of
each firewall. A finding names the verdict and, when it was proven, the attack:
# config/packages/security.yaml
security:
firewalls:
api:
pattern: ^/api/(\w+/?)+$
stateless: true
bin/console regex:security
Firewalls : 2
ReDoS >= : high
Flagged : 1
FAIL 1 firewall regex patterns exceed the ReDoS threshold.
Firewall Regex ReDoS
--------------------
CRIT api (config/packages/security.yaml:4) CRITICAL score 10
↳ Verdict: Exponential backtracking (proven)
↳ Pattern: ^/api/(\w+/?)+$
↳ Attack: "/api/" . "0" x n . "!"
"/api/" . "0" x n . "!" reads as PHP: "/api/" . str_repeat("0", $n) .
"!", a request path that makes preg_match() give up from 19 repetitions
under PHP’s default limits. regex:analyze prints the same finding as
Verdict and Attack rows, and its JSON report carries them as details:
{
"kind": "redos",
"severity": "critical",
"title": "api (config/packages/security.yaml:4)",
"details": [
{"label": "CheckOutcome", "value": "CRITICAL", "kind": "text"},
{"label": "Verdict", "value": "Exponential backtracking (proven)", "kind": "text"},
{"label": "Score", "value": "10", "kind": "text"},
{"label": "Pattern", "value": "^/api/(\\w+/?)+$", "kind": "pattern"},
{"label": "Attack", "value": "\"/api/\" . \"0\" x n . \"!\"", "kind": "text"}
],
"notes": []
}
The ReDoS guide explains the verdicts and the attack.
Each command exits with 0 when it found nothing wrong, 1 when the patterns or the files it judged have a problem, and 2 when an option or the configuration cannot be used (see the CLI guide).
Upgrading from 1.x
See UPGRADE-2.0.md: exclude_paths is now exclude,
analysis.ignore_patterns is merged into redos.ignored_patterns, and
analysis.redos_threshold is gone. A 1.x key stops the container compile with
the key that replaces it.
redos.enabled: true now makes regex:lint report ReDoS findings: 1.x read
the setting and never ran the analysis. Expect new warnings on the first run.