diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index e9d7b82b2..515d40c62 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -14,7 +14,7 @@ class Kernel extends HttpKernel * @var array */ protected $middleware = [ - // \App\Http\Middleware\TrustHosts::class, + \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\TrustProxies::class, \Illuminate\Http\Middleware\HandleCors::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class, diff --git a/app/Http/Middleware/TrustHosts.php b/app/Http/Middleware/TrustHosts.php index c9c58bddc..3d9b77734 100644 --- a/app/Http/Middleware/TrustHosts.php +++ b/app/Http/Middleware/TrustHosts.php @@ -2,7 +2,9 @@ namespace App\Http\Middleware; +use App\Models\InstanceSettings; use Illuminate\Http\Middleware\TrustHosts as Middleware; +use Spatie\Url\Url; class TrustHosts extends Middleware { @@ -13,8 +15,25 @@ class TrustHosts extends Middleware */ public function hosts(): array { - return [ - $this->allSubdomainsOfApplicationUrl(), - ]; + $trustedHosts = []; + // Trust the configured FQDN from InstanceSettings + try { + $settings = InstanceSettings::get(); + if ($settings && $settings->fqdn) { + $url = Url::fromString($settings->fqdn); + $host = $url->getHost(); + if ($host) { + $trustedHosts[] = $host; + } + } + } catch (\Exception $e) { + // If instance settings table doesn't exist yet (during installation), + // fall back to APP_URL only + } + + // Trust all subdomains of APP_URL as fallback + $trustedHosts[] = $this->allSubdomainsOfApplicationUrl(); + + return array_filter($trustedHosts); } } diff --git a/tests/Feature/TrustHostsMiddlewareTest.php b/tests/Feature/TrustHostsMiddlewareTest.php new file mode 100644 index 000000000..2e6169643 --- /dev/null +++ b/tests/Feature/TrustHostsMiddlewareTest.php @@ -0,0 +1,142 @@ + 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + expect($hosts)->toContain('coolify.example.com'); +}); + +it('rejects password reset request with malicious host header', function () { + // Set up instance settings with legitimate FQDN + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + // The malicious host should NOT be in the trusted hosts + expect($hosts)->not->toContain('coolify.example.com.evil.com'); + expect($hosts)->toContain('coolify.example.com'); +}); + +it('handles missing FQDN gracefully', function () { + // Create instance settings without FQDN + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => null] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + // Should still return APP_URL pattern without throwing + expect($hosts)->not->toBeEmpty(); +}); + +it('filters out null and empty values from trusted hosts', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => ''] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + // Should not contain empty strings or null + foreach ($hosts as $host) { + if ($host !== null) { + expect($host)->not->toBeEmpty(); + } + } +}); + +it('extracts host from FQDN with protocol and port', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com:8443'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + expect($hosts)->toContain('coolify.example.com'); +}); + +it('handles exception during InstanceSettings fetch', function () { + // Drop the instance_settings table to simulate installation + \Schema::dropIfExists('instance_settings'); + + $middleware = new TrustHosts($this->app); + + // Should not throw an exception + $hosts = $middleware->hosts(); + + expect($hosts)->not->toBeEmpty(); +}); + +it('trusts IP addresses with port', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'http://65.21.3.91:8000'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + expect($hosts)->toContain('65.21.3.91'); +}); + +it('trusts IP addresses without port', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'http://192.168.1.100'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + expect($hosts)->toContain('192.168.1.100'); +}); + +it('rejects malicious host when using IP address', function () { + // Simulate an instance using IP address + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'http://65.21.3.91:8000'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + // The malicious host attempting to mimic the IP should NOT be trusted + expect($hosts)->not->toContain('65.21.3.91.evil.com'); + expect($hosts)->not->toContain('evil.com'); + expect($hosts)->toContain('65.21.3.91'); +}); + +it('trusts IPv6 addresses', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'http://[2001:db8::1]:8000'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + // IPv6 addresses are enclosed in brackets, getHost() should handle this + expect($hosts)->toContain('[2001:db8::1]'); +});