mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-16 16:45:07 +00:00
1aaab1bf66
The value was read with env() inside the withMiddleware closure in bootstrap/app.php. That closure runs when the HTTP kernel is resolved, which is before the dotenv bootstrapper reads .env — so on every web request env() returned null for anything set in .env, and the proxy was never trusted. It worked when the value came from a real environment variable, which is why the Docker compose path was fine and the manual install described in INSTALL.md, where we tell people to put it in .env, was not. Artisan bootstraps in the other order, so a check from the command line reported the setting as working the whole time. Behind a TLS-terminating proxy the consequence is not subtle. Laravel falls back to the connecting address and the plain scheme, builds every link and redirect with http:// while the browser is on https://, and marks the session cookie non-secure. The browser then declines to send that cookie to what it reads as a different, less secure origin, the session arrives empty, and the first write fails with a 419 that reads as "your session expired" — most often on the create-your-admin form, which is the first thing a new install submits. Afterwards each redirect leaves and re-enters over the wrong scheme, which is the random bounce back to the login screen people report as flakiness. Moved to config/trustedproxy.php, the key the framework's TrustProxies middleware already falls back to on its own. Config files load after dotenv, so the value is there whether it comes from .env or from the environment. This was also the only env() read outside config/, which means config:cache is no longer dangerous on this application. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
89 lines
3.7 KiB
PHP
89 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
/**
|
|
* The trusted proxy list has to be read from configuration, not from
|
|
* bootstrap/app.php.
|
|
*
|
|
* The `withMiddleware` closure runs when the HTTP kernel is resolved, and
|
|
* that happens *before* the dotenv bootstrapper reads .env. Anything set
|
|
* only in .env is therefore invisible to `env()` in that closure, on every
|
|
* web request — while artisan, which bootstraps in the other order, reports
|
|
* the setting as working. That combination is what makes the bug so hard to
|
|
* see from the outside: the operator sets TRUSTED_PROXIES, a CLI check
|
|
* agrees it is set, and the web app ignores it anyway.
|
|
*
|
|
* With the proxy untrusted, Laravel falls back to the connecting address and
|
|
* the plain scheme, so behind a TLS-terminating proxy every generated URL
|
|
* and every redirect comes out as `http://` on a page the browser loaded
|
|
* over `https://`. The browser then refuses to send the session cookie to
|
|
* that other origin, the session looks empty, and the write fails with a 419
|
|
* that reads as "your session expired".
|
|
*/
|
|
beforeEach(function () {
|
|
Route::get('/__proxy-probe', fn () => response()->json([
|
|
'root' => request()->getSchemeAndHttpHost(),
|
|
'ip' => request()->ip(),
|
|
'secure' => request()->isSecure(),
|
|
]));
|
|
});
|
|
|
|
test('forwarded scheme, host and client address are honoured when the proxy is trusted', function () {
|
|
config()->set('trustedproxy.proxies', '*');
|
|
|
|
$this->get('/__proxy-probe', [
|
|
'X-Forwarded-Proto' => 'https',
|
|
'X-Forwarded-Host' => 'files.example.com',
|
|
'X-Forwarded-For' => '203.0.113.9',
|
|
])->assertOk()->assertJson([
|
|
'root' => 'https://files.example.com',
|
|
'ip' => '203.0.113.9',
|
|
'secure' => true,
|
|
]);
|
|
});
|
|
|
|
test('forwarded headers are ignored when no proxy is trusted', function () {
|
|
config()->set('trustedproxy.proxies', null);
|
|
|
|
// Asserted against the forwarded values rather than a literal expected
|
|
// host: the test environment's APP_URL supplies the host here, and
|
|
// isSecure() is already true from it, so neither is a signal on its own.
|
|
// What discriminates is that the proxy's claims are not adopted.
|
|
$json = $this->get('/__proxy-probe', [
|
|
'X-Forwarded-Proto' => 'https',
|
|
'X-Forwarded-Host' => 'files.example.com',
|
|
'X-Forwarded-For' => '203.0.113.9',
|
|
])->assertOk()->json();
|
|
|
|
expect($json['ip'])->toBe('127.0.0.1')
|
|
->and($json['root'])->not->toContain('files.example.com');
|
|
});
|
|
|
|
test('the config key the framework falls back to is wired to TRUSTED_PROXIES', function () {
|
|
// Illuminate\Http\Middleware\TrustProxies reads `trustedproxy.proxies`
|
|
// when nothing called trustProxies(at:). That is the only path that sees
|
|
// a value coming from .env, so the key has to stay spelled this way and
|
|
// has to keep reading that variable. Evaluated directly rather than
|
|
// through config(), which already holds the value loaded at boot.
|
|
$_ENV['TRUSTED_PROXIES'] = '10.0.0.1,10.0.0.2';
|
|
$_SERVER['TRUSTED_PROXIES'] = '10.0.0.1,10.0.0.2';
|
|
|
|
try {
|
|
expect(require base_path('config/trustedproxy.php'))
|
|
->toBe(['proxies' => '10.0.0.1,10.0.0.2']);
|
|
} finally {
|
|
unset($_ENV['TRUSTED_PROXIES'], $_SERVER['TRUSTED_PROXIES']);
|
|
}
|
|
});
|
|
|
|
test('bootstrap/app.php does not read TRUSTED_PROXIES from the environment', function () {
|
|
// Reintroducing this read is the regression: it works when the value is
|
|
// a real environment variable (Docker `environment:`), and silently does
|
|
// nothing when it comes from .env, which is the documented way to set it.
|
|
expect(file_get_contents(base_path('bootstrap/app.php')))
|
|
->not->toContain('TRUSTED_PROXIES');
|
|
});
|