From 4bcdafbeff43d3737820abed6851fe83806cab3d Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 19 Jun 2026 20:35:06 +0200 Subject: [PATCH] fix(tls): force https URL generation for the active domain (external-proxy mode) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behind an external TLS proxy, Clusev's own Caddy only honors the upstream's X-Forwarded-Proto when TRUSTED_PROXY_CIDR is set. Without it the app saw http, asset() emitted http:// URLs, and the HTTPS page's CSP ('self') blocked them — the panel loaded with no CSS/JS over the domain. PanelScheme now forces the URL root + scheme to https:// whenever the active domain is being served, regardless of the (possibly untrusted) request scheme. The bare-IP recovery path returns earlier and stays on HTTP. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 13 +++++++++++++ app/Http/Middleware/PanelScheme.php | 9 +++++++++ config/clusev.php | 2 +- tests/Feature/ExternalTlsModeTest.php | 15 +++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9783b07..b476146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,19 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._ +## [0.9.17] - 2026-06-19 + +### Behoben +- **Panel über die Domain hinter einem externen TLS-Proxy war unbenutzbar (CSS/JS per CSP + blockiert).** Im Externer-Proxy-Modus terminiert der Upstream HTTPS und reicht das Schema per + `X-Forwarded-Proto` weiter; Clusevs eigener Caddy vertraut dem aber nur, wenn `TRUSTED_PROXY_CIDR` + gesetzt ist. War es das nicht, sah die App `http`, erzeugte `http://`-Asset-URLs — und die über + HTTPS ausgelieferte Seite (CSP `'self'`) blockierte diese → kein CSS/JS, kein Login. Bei aktiver + Domain wird die URL-Erzeugung (Assets, Routen) jetzt fest auf `https://` gezwungen + (`URL::forceRootUrl`), unabhängig vom (evtl. nicht vertrauten) Request-Schema. Der Bare-IP- + Recovery-Pfad (HTTP) bleibt unberührt. Hinweis: für korrekte `Secure`-Cookies im Externer-Proxy- + Modus weiterhin `TRUSTED_PROXY_CIDR` auf die Proxy-Adresse setzen. + ## [0.9.16] - 2026-06-19 ### Behoben diff --git a/app/Http/Middleware/PanelScheme.php b/app/Http/Middleware/PanelScheme.php index cd987ec..3a1dc27 100644 --- a/app/Http/Middleware/PanelScheme.php +++ b/app/Http/Middleware/PanelScheme.php @@ -5,6 +5,7 @@ namespace App\Http\Middleware; use App\Services\DeploymentService; use Closure; use Illuminate\Http\Request; +use Illuminate\Support\Facades\URL; use Symfony\Component\HttpFoundation\Response; /** @@ -76,6 +77,14 @@ class PanelScheme return redirect()->to('https://'.$domain.$request->getRequestUri(), 301); } + // Serving the active domain over HTTPS: force generated URLs (assets, routes) to + // https:// so they MATCH the page scheme. Critical in external-proxy mode, where + // the upstream terminates TLS and the forwarded scheme may not be trusted here — without + // this asset() emits http:// URLs that the HTTPS page's CSP ('self') blocks, leaving the + // panel with no CSS/JS. The bare-IP recovery path returned earlier and stays on HTTP. + URL::forceRootUrl('https://'.$domain); + URL::forceScheme('https'); + return $next($request); } } diff --git a/config/clusev.php b/config/clusev.php index 5a15c13..90a8f71 100644 --- a/config/clusev.php +++ b/config/clusev.php @@ -2,7 +2,7 @@ return [ // First tagged release is v0.1.0 (semantic, not -dev). - 'version' => '0.9.16', + 'version' => '0.9.17', // Deployed commit + branch. install.sh bakes these into .env from the host's .git (the prod // image ships no .git); the Versions page prefers them and falls back to a live .git read in diff --git a/tests/Feature/ExternalTlsModeTest.php b/tests/Feature/ExternalTlsModeTest.php index 3c880d0..1ef3bd2 100644 --- a/tests/Feature/ExternalTlsModeTest.php +++ b/tests/Feature/ExternalTlsModeTest.php @@ -99,4 +99,19 @@ class ExternalTlsModeTest extends TestCase $this->expectException(HttpException::class); $this->runPanelScheme(Request::create('http://evil.example.com/dashboard')); } + + public function test_external_mode_forces_https_url_generation_for_the_active_domain(): void + { + Setting::put('panel_domain', 'panel.example.com'); + app(DeploymentService::class)->setTlsMode('external'); + + // External proxy terminates TLS; the app itself sees a plain-HTTP request here. + $response = $this->runPanelScheme(Request::create('http://panel.example.com/dashboard')); + $this->assertSame(200, $response->getStatusCode()); + + // Generated URLs (assets, routes) must be https:// so the HTTPS page's CSP + // ('self') does not block them — the bug that left the panel with no CSS/JS. + $this->assertSame('https://panel.example.com/build/app.css', url('build/app.css')); + $this->assertStringStartsWith('https://panel.example.com', route('login')); + } }