Executive Summary
A critical vulnerability in Perl through version 5.43.9 (CVE-2026-13221) causes the regex engine to produce silently incorrect match results when an alternation contains more than 65,535 fixed-string branches compiled into a trie via Perl_study_chunk. The delta between the first branch and the shared tail overflows the storage type, causing the trie to reference wrong memory positions. The match appears to succeed but evaluates against incorrect data — making any security validator or allowlist built on such a pattern untrustworthy.
CVSS Score: 9.1 (Critical)
Vulnerability Overview
| Attribute | Value |
|---|---|
| CVE ID | CVE-2026-13221 |
| CVSS Score | 9.1 (Critical) |
| Type | Silent Logic Bypass via Integer Overflow in Regex Trie |
| Attack Vector | Depends on context — affects any code using large alternation regexes |
| Privileges Required | None (input-dependent) |
| User Interaction | None |
| Condition | Regex alternation must contain > 65,535 branches compiled into a trie |
Affected Versions
| Component | Affected | Fixed |
|---|---|---|
| Perl | <= 5.43.9 | Patch pending / upstream fix |
Technical Analysis
Perl's regex optimizer (Perl_study_chunk) compiles large alternations — sequences like (aaa|bbb|ccc|...) — into a trie data structure for performance. The trie stores a delta value representing the byte offset from the first branch to the shared tail. When the alternation contains more than 65,535 branches, this delta overflows its storage type, wrapping to a small or negative number and pointing the trie to an incorrect memory location.
The critical consequence is that the match does not fail with an error — it silently evaluates against wrong data. A regex designed to validate input against an exact allowlist of 65,536+ patterns will appear to match but may produce false positives, allowing unexpected strings through.
Vulnerable Code Pattern
# Allowlist with > 65,535 entries (e.g. generated from a large dataset)
my $pattern = join('|', @large_allowlist); # 65,536+ branches
if ($input =~ /^($pattern)$/) {
# This match result may be WRONG — do not trust
process_input($input);
}Attack Scenario
1. Application builds a large regex allowlist (e.g. IP allowlists, domain filters)
2. The alternation branch count exceeds 65,535
3. Perl_study_chunk compiles the trie with an overflowed delta
4. Attacker supplies an input that should NOT match
5. The corrupted trie incorrectly reports a match
6. Attacker bypasses the security controlImpact
| Impact Type | Description |
|---|---|
| Security Filter Bypass | Input validation regexes with large alternations can be silently bypassed |
| Allowlist Circumvention | IP, domain, or pattern allowlists may accept disallowed values |
| Data Integrity | Parsing code that relies on regex matching may process malformed data |
| Invisible Failure | No error or warning is raised — failures are completely silent |
Remediation
Step 1: Identify Vulnerable Patterns
Search your codebase for regex alternations that may be generated from large datasets:
# Find files building regex patterns from arrays/lists
grep -rn 'join.*|' --include="*.pl" --include="*.pm" .
grep -rn 'join.*|' --include="*.cgi" .
# Look for alternations in regex patterns
grep -rn '=~\s*/\^(' --include="*.pl" .Step 2: Apply Patches
Monitor the Perl security mailing list and your distribution's security advisories:
# Check current Perl version
perl -v
# Debian/Ubuntu
apt-get update && apt-get upgrade perl
# RHEL/CentOS/Fedora
dnf update perl
# Check for available patches
perl -MCPAN -e 'install Perl'Step 3: Refactor Large Alternation Patterns
Until a patch is available, restructure regex validators to avoid > 65,535 alternation branches:
# AVOID: Single regex with 65,536+ branches
my $re = join('|', @huge_list);
return $input =~ /^($re)$/;
# PREFER: Hash-based lookup (faster and not affected by this bug)
my %allowed = map { $_ => 1 } @huge_list;
return exists $allowed{$input};
# PREFER: Split large alternations into smaller batches
sub validate_input {
my ($input, @list) = @_;
my $chunk_size = 1000;
while (my @chunk = splice(@list, 0, $chunk_size)) {
my $re = join('|', map { quotemeta } @chunk);
return 1 if $input =~ /^(?:$re)$/;
}
return 0;
}Step 4: Runtime Mitigation
# Add assertion to catch unexpected branch counts during development
sub safe_alternation {
my @branches = @_;
die "Alternation branch count exceeds safe threshold: " . scalar(@branches)
if @branches > 60_000;
return join('|', map { quotemeta } @branches);
}Detection
| Indicator | Description |
|---|---|
| Unexpected input passing regex allowlists | May indicate trie overflow producing false positives |
| Access log anomalies with disallowed values | Inputs that should be rejected appearing in application logs |
| Regex alternations with 65,000+ branches | Grep for join.*| with large source arrays |
Post-Remediation Steps
- Audit all large alternation regexes across Perl codebases — replace with hash lookups where feasible
- Upgrade Perl as soon as a patched version is available from your distribution
- Monitor Perl security advisories — sign up at https://perl.org/security
- Add unit tests that verify regex allowlists correctly reject disallowed inputs
- Log all validation decisions at a debug level to detect unexpected passes