HttpFoundation getBaseUrl: what I would change after GHSA-5mx8

HttpFoundation getBaseUrl: what I would change after GHSA-5mx8

GHSA-5mx8-mwrp-5693 lets Request::getBaseUrl() keep a client-chosen host when the URI starts with // or /\. composer audit on 7.4.22 printed four hits. I would bump to 7.4.23 / 6.4.50 / 8.1.11, or 5.4.56. This is not a Laravel-core CVE.

· 9 min read #php #security #laravel #symfony #composer

 GHSA-5mx8 is a Symfony HttpFoundation lock bump, not Laravel core

Caption: composer show first. Then composer audit --locked. Then getBaseUrl() on a throwaway Request. Illuminate always pulls symfony/http-foundation. Do not invent a CVE id.

I did not send a crafted //host URL at anything tonight. I opened Ubuntu 24.04.5, installed archive php8.3-cli, and ran throwaway composer require of two tags. php -v printed PHP 8.3.6, built 19 Oct 2026. PHP_VERSION_ID is 80306. Package 8.3.6-0ubuntu0.24.04.13. There is no php-fpm on this box. Distro Composer is still 2.7.1-2.

On 7.4.22, Request::getBaseUrl() returned //evil.example/public/index.php while getHost() stayed app.example. On 7.4.23 the same constructor returned an empty base URL. composer audit on 7.4.22 printed four advisories. On 7.4.23 it printed No security vulnerability advisories found.

That is the wall this week. APP_URL and framework.trusted_hosts do not close it — the advisory says the request host stays the real one. Laravel 13.42.0 (10 Nov 2026) still requires symfony/http-foundation: ^7.4.0 || ^8.0.0. This is a Composer component advisory. It is not laravel/framework. The GitHub advisory has no CVE id. I will not invent one.

What changed

Symfony’s GHSA-5mx8 blog is dated 4 Nov 2026. GHSA-5mx8-mwrp-5693 published the same day. Package: symfony/http-foundation. Severity Moderate. CWE-601 and CWE-829. The advisory table has no CVE. Packagist PKSA-b2gg-vk1g-6d5q (reportedAt 2026-11-04 00:00:00) is the same write-up.

What I will treat as locked this week:

  • Request::getBaseUrl() kept a client-chosen prefix. When it does not find the script name at the start of the request URI, it looks further into the path — the support path for a front controller in a subdirectory. A URI that starts with // or /\ made that prefix start with a host. With SCRIPT_NAME=/public/index.php, the advisory’s example request for //evil.example/public/index.php/login is routed to /login, with base URL //evil.example/public/index.php and base path //evil.example/public. Generated path() links, form actions, redirectToRoute() targets, and relative asset() URLs then point at that host. The user has to follow a crafted link. trusted_hosts does not help.
  • Patched floors are 5.4.56 / 6.4.50 / 7.4.23 / 8.1.11. Affected (GHSA): <5.4.56, >=6 <6.4.50, >=7 <7.4.23, >=8 <8.1.11. Resolution: getBaseUrl() now returns empty when the request URI starts with // or /\. Forward-ported to 8.2 in the advisory. Packagist still has 8.2.0-BETA1 only (time 2026-11-04T08:52:03Z). symfony.com/releases still lists 8.2 as Under development, November 2026 (retrieved 13 Nov 2026). I will not pin a beta.
  • Packagist time on the four floors, 4 Nov 2026: 5.4.56 07:37:33Z, 6.4.50 08:16:31Z, 7.4.23 08:27:22Z, 8.1.11 08:39:20Z. GitHub symfony/http-foundation tags the same day: v5.4.56 10:52 UTC, v6.4.50 11:25, v7.4.23 11:29, v8.1.11 11:32. Symfony blogs: 6.4.50 and 7.4.23 on 4 Nov; 8.1.11 metapackage on 5 Nov. The 6.4.50 post says 6.4.50 is the last version for the 6.4 branch (bug fixes until November 2026; security until November 2027).
  • No later stable tag I can cite. Packagist retrieved 13 Nov 2026: no 8.1.12, no 7.4.24, no 6.4.51, no 5.4.57, no 8.2.0. 8.1.11 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.42.0 (time 2026-11-10T15:54:30Z) still 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.

What I will treat as rumor: Illuminate having a CVE; APP_URL as the patch; Symfony 8.2 as a GA I can pin; a CVE id for GHSA-5mx8; apt upgrade on Ubuntu 24.04 fixing this (distro PHP is still 8.3.6; HttpFoundation is Composer).

Same-day siblings on the same http-foundation tags, only if you are reading this lock: GHSA-3h27-hjv7-fqjx (Low; getPathInfo() stripped a script-name prefix from /index.phpadmin; no CVE); CVE-2026-118514 / GHSA-jrf8-gvm4-7cgh (Moderate 6.3; HEAD bypasses GET-only RequestMatcher / access_control; Symfony blog 4 Nov, NVD listed 6 Nov; Laravel rarely uses Symfony Security access_control); GHSA-xg2q-4rrq-657q (private_ranges trusted proxies; Moderate 6.3; http-foundation >=7.2 <7.4.23 and >=8 <8.1.11). The 4 Nov dump also tagged Serializer / Routing / HtmlSanitizer CVEs — composer show those packages; do not invent they ship in Laravel core. This blog’s lib/ pulled yaml 7.4.23 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 //host URL at production.”

1. Prove the lock. Then run the getBaseUrl lab. Do not send a crafted URI at a live app.

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. On this 8.3.6 CLI, throwaway 7.4.22 printed four rows (GHSA-5mx8 with NO CVE, GHSA-3h27, GHSA-xg2q, CVE-2026-118514). Throwaway 7.4.23 printed none. Composer 2.7.1 was enough to see that.

Then the before, on the binary that actually served the last request. Request::create('//evil.example/...') is the wrong constructor here — it parsed the host as evil.example. The advisory case keeps HTTP_HOST as the real app and puts // on REQUEST_URI:

use Symfony\Component\HttpFoundation\Request;

$server = [
    'HTTP_HOST' => 'app.example',
    'REQUEST_URI' => '//evil.example/public/index.php/login',
    'SCRIPT_NAME' => '/public/index.php',
    'SCRIPT_FILENAME' => '/var/www/app/public/index.php',
    'REQUEST_METHOD' => 'GET',
];

$req = new Request([], [], [], [], [], $server);
var_export($req->getBaseUrl());
echo PHP_EOL, 'host=', $req->getHost(), PHP_EOL;
echo 'pathInfo=', $req->getPathInfo(), PHP_EOL;

On this box that printed:

PHP 8.3.6 (cli) (built: Oct 19 2026)
# throwaway composer require symfony/http-foundation:7.4.22
'//evil.example/public/index.php'
host=app.example
pathInfo=/login

# same tree, 7.4.23
''
host=app.example
pathInfo=//evil.example/public/index.php/login

Empty base URL is the fix. A base URL that starts with // is 7.4.22. getHost() stayed app.example on both tags — that is why trusted_hosts does not close it. I would not ask a browser to follow that URI.

2. Bump to the 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.23

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

sudo systemctl reload php8.3-fpm

As of 13 Nov 2026 I still want 7.4.23, 6.4.50, 8.1.11, or 5.4.56. I would not invent 8.1.12. I would not install 8.2.0-BETA1.

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 httpFoundationGhsa5mx8FloorMet(): bool
{
    if (! InstalledVersions::isInstalled('symfony/http-foundation')) {
        return false;
    }

    $v = ltrim(InstalledVersions::getPrettyVersion('symfony/http-foundation'), 'v');
    if ($v === '' || preg_match('/(?:alpha|beta|rc|dev)/i', $v)) {
        return false;
    }

    return match (true) {
        str_starts_with($v, '8.') => version_compare($v, '8.1.11', '>='),
        str_starts_with($v, '7.') => version_compare($v, '7.4.23', '>='),
        str_starts_with($v, '6.') => version_compare($v, '6.4.50', '>='),
        str_starts_with($v, '5.') => version_compare($v, '5.4.56', '>='),
        default => false,
    };
}

// Before — generated path() / form action used getBaseUrl() as-is.
// $request = Request::createFromGlobals();
// url('/login');

// After — brake until the lock is on a floor tag. The patch is Composer, not this if.
if (! httpFoundationGhsa5mx8FloorMet()) {
    abort(503, 'HttpFoundation below GHSA-5mx8-mwrp-5693 floor.');
}

$request = Request::createFromGlobals();

I ran that helper here. 7.4.22 / 8.1.10 / 6.4.49 / 5.4.54 / 8.2.0-BETA1 → false. 7.4.23 / 8.1.11 / 6.4.50 / 5.4.56 → true. That 503 is a brake. The patch is the lockfile. If I am already on 7.4.23 I delete the gate. I would still composer show symfony/yaml, symfony/routing, symfony/serializer, and symfony/html-sanitizer once. Absent packages are not those 4 Nov CVEs.

What would bite on a self-hosted VPS

 Lock check, audit, then getBaseUrl empty vs a host prefix

Caption: composer show then composer audit --locked. Then getBaseUrl() with SCRIPT_NAME=/public/index.php and a REQUEST_URI that starts with //. Empty string is the fix. A //host/... base URL is 7.4.22. Do not send that URI at a live app.

First bite: a root .htaccess that rewrites everything into public/. The advisory names that layout, nginx without fastcgi_split_path_info, and php -S with a router outside the document root. Symfony’s documented configs are not affected. I would still bump. A Laravel VPS that points Nginx at public/ the way the Laravel nginx gist does is the safer layout — it is not a reason to stay on 7.4.22.

Second bite: PHP 8.3 vs 8.4 on the same app. Laravel 13 advertises 8.3–8.5. 8.1.11 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. 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.11 said so)

I would pin the 7.4 line on an 8.3 box: composer require "symfony/http-foundation:^7.4.23". 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 Request.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.22. OPcache keeps the old class until I reload the pool Nginx actually uses. This lab has no FPM; the command I would type on a real VPS is still php-fpm8.3 -v then systemctl reload.

Nine days after the advisory

 Advisory timeline from 4 Nov tags to this 13 Nov lab note

Caption: Packagist floors 4 Nov 2026. GHSA published the same day, Moderate, no CVE. 8.1.11 metapackage blog 5 Nov. I am writing 13 Nov. Packagist still has 8.1.11 / 7.4.23 / 6.4.50 / 5.4.56 as latest. No 8.1.12. 8.2 is still BETA1.

Symfony’s 6.4 page still lists 6.4.50 as latest 6.4, last bugfix tag, security until November 2027 (retrieved 13 Nov 2026). Stable current is 8.1.11. LTS current is 7.4.23. Honest target this week: Laravel 13 on PHP 8.3 FPM, lock on http-foundation 7.4.23, getBaseUrl() empty on that REQUEST_URI, CLI and the socket on the same PHP. If the box is already 8.4, 8.1.11. 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.23 (or 6.4.50 / 8.1.11 / 5.4.56 on that line) and getBaseUrl() returns empty on the lab constructor, you are on this. If audit is red on 7.4.22 and the lock still says that tag, you are not. If CLI shows 7.4.23 and the deployed vendor/ is still 7.4.22, 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 getBaseUrl() returning //evil.example/public/index.php on 7.4.22 while getHost() stayed app.example, or on Composer blocking 8.1.11 because this VPS is PHP 8.3.6. Did you hit the same thing, or a different one — a root .htaccess rewrite into public/, nginx without fastcgi_split_path_info, a lock that jumped to Symfony 8 on a laptop with 8.4, a host that will not let you update vendor/? 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.