HttpFoundation Cache-Control: what I would change after CVE-2026-108082

HttpFoundation Cache-Control: what I would change after CVE-2026-108082

CVE-2026-108082 is a ReDoS in HeaderUtils::split() on Cache-Control. composer audit can pass on 7.4.19 while split('foo bar', ' ') still returns one part. I would bump to 7.4.21 / 6.4.48 / 8.1.9, or 5.4.54. This is not a Laravel-core CVE.

· 6 min read #php #security #laravel #symfony #composer #cve

 CVE-2026-108082 is a Symfony HttpFoundation lock bump, not Laravel core

Caption: composer show first. Then composer audit --locked. Then the space-split lab. Illuminate always pulls symfony/http-foundation. Do not stop at 7.4.19.

I did not send a crafted Cache-Control header at anything tonight. I opened Ubuntu 24.04 archive PHP 8.3.6 (php -v built 19 Oct 2026, package 8.3.6-0ubuntu0.24.04.13) and ran throwaway composer require of two tags. HeaderUtils::split('foo bar', ' ') on 7.4.19 returned one part. The same call on 7.4.21 returned two. composer audit on 7.4.19 printed No security vulnerability advisories found.

That is the wall this week. Packagist treats the first ReDoS tag as patched. The whitespace-separator follow-up is a later tag. Laravel 13.38.0 (20 Oct 2026) still requires symfony/http-foundation: ^7.4.0 || ^8.0.0. This is a Composer component CVE. It is not laravel/framework. composer why symfony/http-foundation on a Laravel 13 tree points at Illuminate. On this blog’s lib/ it printed Package not found — Spatie pulled yaml, not HttpFoundation.

What changed

Symfony’s CVE blog is dated 15 Oct 2026. GHSA-r925-mgvr-h9cc published the same day. Package: symfony/http-foundation. CVSS 8.7 High (CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N). CWE-1333. Availability only. Unauthenticated.

What I will treat as locked this week:

  • HeaderUtils::split() was the engine. The token alternative also matched whitespace, then a (?<!\s) lookbehind. A run of spaces before a separator could be divided in exponentially many ways. HeaderBag calls that splitter for Cache-Control whenever a Request is built, so a 404 still does the work. Advisory timing: 813 ms for 17 spaces in an 8 KB header on PHP 8.5, doubling with each extra space. From 18 spaces, preg_match_all() hits pcre.backtrack_limit and split() returns an empty array — every directive of the header is silently dropped. My CLI pcre.backtrack_limit is 1000000. I did not reproduce the 813 ms. I did not paste that header.
  • First ReDoS tags are not the floor I would install. Affected (GHSA): >=4.1.0 <5.4.54, >=6 <6.4.46, >=7 <7.4.19, >=8 <8.1.7. Patched in that advisory’s table: 5.4.54 / 6.4.46 / 7.4.19 / 8.1.7. Packagist time on those first 6.4/7.4/8.1 tags is 14 Sep 2026. Packagist advisory PKSA-xmwy-rmx5-m7h4 still ranges <7.4.19 / <6.4.46 / <8.1.7 / <5.4.54. That is why composer audit goes quiet on 7.4.19.
  • The first fix broke splitting on a space. Official resolution: HeaderUtils::split('foo bar', ' ') returned a single value in 6.4.46–47, 7.4.19–20, 8.1.7–8. Whitespace used as a separator is left out of the whitespace branch in 6.4.48 / 7.4.21 / 8.1.9. Symfony blogs for those three tags: 14 Oct 2026. GitHub: v6.4.48 14 Oct 06:43 UTC, v7.4.21 06:46, v8.1.9 06:49. PRs #66048 and follow-up #66491.
  • 5.4.54 ships both PRs in one tag. GitHub v5.4.54 is 15 Oct 2026 06:52 UTC (#66048 and #66491). Packagist time 2026-09-30T11:07:08+00:00 (git). No 5.4.55 I can cite.
  • CVE record 15 Oct 2026 14:46:01Z (cve.org). NVD 15 Oct 2026 15:17:22 UTC. OSV modified 21 Oct 2026. Packagist still has no 8.1.10 / 7.4.22 / 6.4.49. 8.1.9 needs PHP >= 8.4.1; this 8.3.6 box cannot take it. The 7.4 line is PHP >= 8.2.
  • Laravel always pulls this package. laravel/framework 13.38.0 requires symfony/http-foundation: ^7.4.0 || ^8.0.0. Not a Laravel-core CVE. A CodeCanyon leftover on Laravel 12 will still have the component. composer show is the check, not a Slack thread that says “Laravel is owned.”

What I will treat as rumor: Illuminate having a CVE (composer show laravel/framework is the wrong package); stopping at 7.4.19 because audit is green (my lab: audit clean, split still one part); apt upgrade on Ubuntu 24.04 fixing this (distro PHP is still 8.3.6; HttpFoundation is Composer); Symfony 8.2 as a GA I can pin (forward-ported in the advisory, Packagist still 8.2.x-dev).

Same-week siblings, only if the lock has them: YAML CVE-2026-109413 (floors 5.4.54 / 6.4.45 / 7.4.18 / 8.1.6) and ExpressionLanguage CVE-2026-109449 (complete in 5.4.54 / 6.4.48 / 7.4.21 / 8.1.9). A Week of Symfony #1033 listed all three. This blog’s lib/ pulled yaml 7.4.21 via Spatie and has no http-foundation. I asked composer show. That is not a Laravel app.

What I would change in a real Laravel/PHP app this week

Three edits. None of them is “paste a Cache-Control header at production.”

1. Prove the lock. Then run the space-split lab. Do not send a ReDoS payload.

php -v
php-fpm8.3 -v   # the pool Nginx uses, not the CLI I wish I had
composer show symfony/http-foundation
composer why symfony/http-foundation
composer audit --locked

composer audit talks to Packagist by default. --locked reads composer.lock. Exit 0 means no policy hits. Exit 1 means it found something, or a required package is missing. Composer 2.10.3 (27 Aug 2026) refused to install 7.4.18 here because of PKSA-xmwy-rmx5-m7h4. It was happy to install 7.4.19. Then audit on 7.4.19 was green. That is the miss.

Then the before, on the binary that actually served the last request:

use Symfony\Component\HttpFoundation\HeaderUtils;

$parts = HeaderUtils::split('foo bar', ' ');
var_export($parts);
echo 'count=', count($parts), PHP_EOL;

On this box that printed:

PHP 8.3.6 (cli) (built: Oct 19 2026)
# throwaway composer require symfony/http-foundation:7.4.19
array (
  0 => 'foo bar',
)
count=1

# same tree, 7.4.21
array (
  0 => 'foo',
  1 => 'bar',
)
count=2

Two parts is the complete fix. One part is the first-fix regression. I would not put 17 spaces into a header to “see the 813 ms.”

2. Bump to the complete floor for the line I actually lock. Reload FPM.

# PHP 8.3 VPS — Laravel 13 will resolve the 7.4 line.
composer update symfony/http-foundation --with-dependencies
composer show symfony/http-foundation
# versions : * v7.4.21

# PHP 8.4+ only — 8.1.9 requires >=8.4.1
# composer show will print v8.1.9 when the platform allows it

sudo systemctl reload php8.3-fpm

As of 23 Oct 2026 I still want 7.4.21, 6.4.48, 8.1.9, or 5.4.54. I would not stop at 7.4.19 / 6.4.46 / 8.1.7. I would not invent 8.1.10.

3. Gate any leftover that constructs a Request on an old lock. Do not call this a Laravel CVE.

use Composer\InstalledVersions;
use Symfony\Component\HttpFoundation\Request;

function httpFoundationCompleteFloorMet(): bool
{
    if (! InstalledVersions::isInstalled('symfony/http-foundation')) {
        return false;
    }

    $v = ltrim(InstalledVersions::getPrettyVersion('symfony/http-foundation'), 'v');

    return match (true) {
        str_starts_with($v, '8.') => version_compare($v, '8.1.9', '>='),
        str_starts_with($v, '7.') => version_compare($v, '7.4.21', '>='),
        str_starts_with($v, '6.') => version_compare($v, '6.4.48', '>='),
        str_starts_with($v, '5.') => version_compare($v, '5.4.54', '>='),
        default => false,
    };
}

// Before — every hit built a Request, including 404, including healthchecks.
// $request = Request::createFromGlobals();

// After — brake until the lock is on a complete tag. The patch is Composer, not this if.
if (! httpFoundationCompleteFloorMet()) {
    abort(503, 'HttpFoundation below CVE-2026-108082 complete floor.');
}

$request = Request::createFromGlobals();

That 503 is a brake. The patch is the lockfile. If I am already on 7.4.21 I delete the gate. I would still composer show symfony/yaml and composer show symfony/expression-language once. Absent packages are not those CVEs. Present ones need yaml 7.4.18+ and expression-language 7.4.21 on this line.

What would bite on a self-hosted VPS

 Lock check, audit, then HeaderUtils::split on a space separator

Caption: composer show then composer audit --locked. Then HeaderUtils::split('foo bar', ' '). Two parts is the complete fix. One part is 7.4.19. Do not send Cache-Control at a live app.

First bite: audit green, lock still on 7.4.19. Packagist’s range ends at the first ReDoS tag. CI that only checks composer audit --locked will ship the regression. The space-split is two arguments, not a network request.

Second bite: PHP 8.3 vs 8.4 on the same app. Laravel 13 advertises 8.3–8.5. 8.1.9 will not install on this 8.3.6 CLI. config.platform.php set to 8.4 on a laptop, and FPM still on 8.3, is how I get a lock the VPS cannot install. CI that resolves against 8.4 and deploys to 8.3 will fail on the VPS, or worse, never run composer install there and keep serving laptop vendor/. The error I actually expect:

Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires php >=8.4.1 (because 8.1.9 said so)

I would pin the 7.4 line on an 8.3 box: composer require "symfony/http-foundation:^7.4.21". I would not invent that apt will give me PHP 8.4 this afternoon.

Third bite: shared hosting and leftover vendor/. If the panel will not let me composer update, I do not edit HeaderUtils.php in place. There is no userland polyfill for this advisory on a shared-hosting panel that blocks Composer. Rsync an old vendor/ after a green laptop audit and FPM still runs 7.4.19. OPcache keeps the old class until I reload the pool Nginx actually uses.

Eight days after the advisory

 Advisory timeline from 14 Sep first tags to this 23 Oct lab note

Caption: First ReDoS tags 14 Sep 2026. Complete floors GitHub-released 14 Oct. GHSA and CVE record 15 Oct. I am writing 23 Oct. Packagist still has 8.1.9 / 7.4.21 / 6.4.48 as latest. No 8.1.10.

Symfony’s 6.4 page still lists 6.4.48 as latest 6.4, bug fixes until November 2026, security until November 2027 (retrieved 23 Oct 2026). Honest target this week: Laravel 13 on PHP 8.3 FPM, lock on http-foundation 7.4.21, split('foo bar', ' ') returning two parts, CLI and the socket on the same PHP. If the box is already 8.4, 8.1.9. I would not invent the next Thursday tag today.

Are you on this in production?

If composer show symfony/http-foundation already prints 7.4.21 (or 6.4.48 / 8.1.9 / 5.4.54 on that line) and HeaderUtils::split('foo bar', ' ') returns two parts, you are on this. If audit is green and the lock still says 7.4.19, you are not — that is the first-fix tag Packagist already considers patched. If CLI shows 7.4.21 and the deployed vendor/ is still 7.4.18, you are not. Shared hosting that will not run Composer is the fourth camp. I did not flip a client cluster tonight. I want to know which of those you are in.

Did you hit the same wall?

I would get stuck on composer audit exit 0 on 7.4.19 while split('foo bar', ' ') still returned one part, or on Composer blocking 8.1.9 because this VPS is PHP 8.3.6. Did you hit the same thing, or a different one — a lock that jumped to Symfony 8 on a laptop with 8.4, a host that will not let you update vendor/, yaml present and http-foundation already bumped? Tell me in the comments. I read them.

If the blocker is the VPS itself (wrong PHP floor, a panel that will not run Composer, a socket that still loads old vendor/), I do this class of bump for a living: Hire for deploy.

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.