Harden against host header injection and open redirect (CVE-2025-50578)#1568
Merged
Conversation
Heimdall trusted the incoming X-Forwarded-Host header for URL generation, so a spoofed value poisoned the page base href, asset() URLs and redirect targets - loading assets from and redirecting to an attacker-controlled host (CVE-2025-50578). - TrustProxies no longer trusts X-Forwarded-Host; a forged value can no longer influence getHost(), url(), asset() or redirects. X-Forwarded-For/Port/Proto handling is unchanged. - Trusted proxies are now configurable via the TRUSTED_PROXIES env var (comma-separated CIDRs/IPs, "*" to trust all), defaulting to the previous private ranges. - Added an opt-in TRUSTED_HOSTS allow-list: when set, only the listed hosts are served and any other Host header is rejected. Unset keeps the historic behaviour of serving arbitrary hosts, so existing installs are unaffected. Resolves #1451
Closed
The custom TrustHosts middleware only overrode hosts(), so it inherited the parent's shouldSpecifyTrustedHosts() gate, which skips enforcement whenever the app runs in the local environment or under the test runner. Heimdall ships APP_ENV=local by default (.env.example, copied to .env on install), so the TRUSTED_HOSTS allow-list a user configures per the .env.example guidance was never actually applied. Override shouldSpecifyTrustedHosts() to tie enforcement to configuration instead of environment: apply the allow-list whenever TRUSTED_HOSTS is set, in any environment; when it is unset hosts() is empty and enforcement stays off, preserving the historic no-restriction behaviour. Add handle()-driven tests covering both the configured and unconfigured cases.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Resolves #1451 (CVE-2025-50578).
Heimdall trusted attacker-controlled host headers for URL generation.
layouts/app.blade.phprenders<base href="{{ url('') }}">(request-host-based) whenAPP_URLis left at its default, and views build absolute URLs withasset(). BecauseTrustProxiestrustedX-Forwarded-Hostover broad private proxy ranges, a request like:made every relative asset load from
evil.com, andredirect(route('dash'))emitted aLocationpointing at the spoofed host (the "open redirect").Change (backward-compatible by default)
X-Forwarded-HostinTrustProxies— a forged value can no longer influencegetHost(),url(),asset()or redirects.X-Forwarded-For/-Port/-Protohandling is unchanged.TRUSTED_PROXIESenv var — reverse-proxy IPs/CIDRs are now configurable (comma-separated,*to trust all), defaulting to the previous private ranges when unset.TRUSTED_HOSTSenv var (opt-in) — a newApp\Http\Middleware\TrustHostsrestricts the Host header to a configured allow-list. Unset ⇒ no restriction (Heimdall keeps serving arbitrary hosts, so existing installs are unaffected). Set to your domain ⇒ any other Host is rejected (HTTP 400), fully closing the direct-Host-injection vector.Installs that already set
APP_URLwere partially protected (Heimdall callsURL::forceRootUrl(config('app.url'))); this change closes the remaining header-trust gap and gives operators an explicit lockdown knob.Tests
tests/Feature/TrustProxiesTest.php+tests/Feature/TrustHostsTest.php(12 tests): X-Forwarded-Host is ignored while X-Forwarded-Proto still works,TRUSTED_PROXIESparsing incl.*,TRUSTED_HOSTSempty ⇒ arbitrary host accepted,TRUSTED_HOSTS=example.com⇒evil.comrejected /example.comaccepted, and the subclass is registered in the global middleware stack. Full suite green (46 passing, 1 pre-existing skip);phpcsclean.Notes
TRUSTED_HOSTSallow-list follows Laravel's standardTrustHostsbehaviour and only enforces whenAPP_ENVis notlocal. TheX-Forwarded-Hostchange (the primary mitigation) applies in all environments.TRUSTED_PROXIES/TRUSTED_HOSTSare read viaenv(); if you runphp artisan config:cachethe defaults apply. The security-critical default (not trustingX-Forwarded-Host) is hard-coded and unaffected.