From 2f5c0130f4c08daf9dc4d621fb33f98f02a7d0b8 Mon Sep 17 00:00:00 2001 From: jekkos Date: Wed, 13 May 2026 09:03:32 +0200 Subject: [PATCH] feat: add ALLOWED_HOSTNAMES environment variable support for Docker/Compose (#4544) Allow configuring allowed hostnames via ALLOWED_HOSTNAMES environment variable as an alternative to app.allowedHostnames in .env file. This is more convenient for Docker/Compose deployments where environment variables are set directly in compose files. The ALLOWED_HOSTNAMES variable takes precedence over app.allowedHostnames if both are set, allowing deployment-specific overrides. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Ollama Co-authored-by: Sisyphus --- .env.example | 3 ++ app/Config/App.php | 14 ++++-- docker-compose.dev.yml | 1 + docker-compose.yml | 1 + tests/Config/AppTest.php | 103 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index e51bf8067..1f5cc21c7 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,9 @@ CI_ENVIRONMENT = production # Configure with comma-separated list of domains/subdomains: # app.allowedHostnames = 'yourdomain.com,www.yourdomain.com' # +# Or via environment variable (useful for Docker/Compose): +# ALLOWED_HOSTNAMES=yourdomain.com,www.yourdomain.com +# # For local development: # app.allowedHostnames = 'localhost' # diff --git a/app/Config/App.php b/app/Config/App.php index db4b0a876..23cbe016b 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -58,9 +58,9 @@ class App extends BaseConfig * Allowed Hostnames in the Site URL other than the hostname in the baseURL. * If you want to accept multiple Hostnames, set this. * - * E.g., - * When your site URL ($baseURL) is 'http://example.com/', and your site - * also accepts 'http://media.example.com/' and 'http://accounts.example.com/': + * Or via environment variable (useful for Docker/Compose): + * ALLOWED_HOSTNAMES=example.com,www.example.com + * * ['media.example.com', 'accounts.example.com'] * * @var list @@ -286,7 +286,11 @@ class App extends BaseConfig // Solution for CodeIgniter 4 limitation: arrays cannot be set from .env // See: https://github.com/codeigniter4/CodeIgniter4/issues/7311 - $envAllowedHostnames = getenv('app.allowedHostnames'); + // Support both: app.allowedHostnames (from .env) and ALLOWED_HOSTNAMES (from environment/Docker) + $envAllowedHostnames = getenv('ALLOWED_HOSTNAMES'); + if ($envAllowedHostnames === false || trim($envAllowedHostnames) === '') { + $envAllowedHostnames = getenv('app.allowedHostnames'); + } if ($envAllowedHostnames !== false && trim($envAllowedHostnames) !== '') { $this->allowedHostnames = array_values(array_filter( array_map('trim', explode(',', $envAllowedHostnames)), @@ -327,7 +331,7 @@ class App extends BaseConfig $errorMessage = 'Security: allowedHostnames is not configured. ' . 'Host header injection protection is disabled. ' . - 'Set app.allowedHostnames in your .env file. ' . + 'Set app.allowedHostnames in your .env file or ALLOWED_HOSTNAMES environment variable. ' . 'Example: app.allowedHostnames = "example.com,www.example.com" ' . 'Received Host: ' . $httpHost; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 56d3bb78e..3b0a2f2c6 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -46,6 +46,7 @@ services: - .:/app environment: - CI_ENVIRONMENT=development + - ALLOWED_HOSTNAMES=localhost - MYSQL_USERNAME=admin - MYSQL_PASSWORD=pointofsale - MYSQL_DB_NAME=ospos diff --git a/docker-compose.yml b/docker-compose.yml index e1ebc9791..5cebe0232 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: - logs:/app/writable/logs environment: - CI_ENVIRONMENT=production + - ALLOWED_HOSTNAMES=localhost - FORCE_HTTPS=false - PHP_TIMEZONE=UTC - MYSQL_USERNAME=admin diff --git a/tests/Config/AppTest.php b/tests/Config/AppTest.php index 601378ca7..11c572f86 100644 --- a/tests/Config/AppTest.php +++ b/tests/Config/AppTest.php @@ -18,6 +18,7 @@ class AppTest extends CIUnitTestCase // Clean up environment putenv('CI_ENVIRONMENT'); putenv('app.allowedHostnames'); + putenv('ALLOWED_HOSTNAMES'); unset($_SERVER['HTTP_HOST']); } @@ -281,4 +282,106 @@ class AppTest extends CIUnitTestCase putenv('app.allowedHostnames'); putenv('CI_ENVIRONMENT'); } + + public function testAllowedHostnamesEnvVarParsedAsCommaSeparated(): void + { + // Set ALLOWED_HOSTNAMES environment variable + putenv('ALLOWED_HOSTNAMES=example.com,www.example.com,demo.example.com'); + + $_SERVER['HTTP_HOST'] = 'www.example.com'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['HTTPS'] = null; + + $app = new App(); + + // Constructor should parse comma-separated values + $this->assertEquals(['example.com', 'www.example.com', 'demo.example.com'], $app->allowedHostnames); + $this->assertStringContainsString('www.example.com', $app->baseURL); + + // Clean up + putenv('ALLOWED_HOSTNAMES'); + } + + public function testAllowedHostnamesEnvVarTakesPrecedenceOverDotEnv(): void + { + // Set both environment variables + putenv('ALLOWED_HOSTNAMES=allowed1.com,allowed2.com'); + putenv('app.allowedHostnames=dotenv1.com,dotenv2.com'); + + $_SERVER['HTTP_HOST'] = 'allowed1.com'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['HTTPS'] = null; + + $app = new App(); + + // ALLOWED_HOSTNAMES should take precedence + $this->assertEquals(['allowed1.com', 'allowed2.com'], $app->allowedHostnames); + $this->assertStringContainsString('allowed1.com', $app->baseURL); + + // Clean up + putenv('ALLOWED_HOSTNAMES'); + putenv('app.allowedHostnames'); + } + + public function testAllowedHostnamesEnvVarFallsBackToDotEnv(): void + { + // Only set app.allowedHostnames, not ALLOWED_HOSTNAMES + putenv('app.allowedHostnames=dotenv1.com,dotenv2.com'); + + $_SERVER['HTTP_HOST'] = 'dotenv1.com'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['HTTPS'] = null; + + $app = new App(); + + // Should fall back to app.allowedHostnames + $this->assertEquals(['dotenv1.com', 'dotenv2.com'], $app->allowedHostnames); + $this->assertStringContainsString('dotenv1.com', $app->baseURL); + + // Clean up + putenv('app.allowedHostnames'); + } + + public function testAllowedHostnamesEnvVarTrimmedWhitespace(): void + { + // Set environment variable with whitespace + putenv('ALLOWED_HOSTNAMES= example.com , www.example.com , demo.example.com '); + + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['HTTPS'] = null; + + $app = new App(); + + // Values should be trimmed + $this->assertEquals(['example.com', 'www.example.com', 'demo.example.com'], $app->allowedHostnames); + + // Clean up + putenv('ALLOWED_HOSTNAMES'); + } + + public function testAllowedHostnamesEnvVarFiltersEmptyEntries(): void + { + // Trailing comma should not produce empty entry + putenv('ALLOWED_HOSTNAMES=example.com,'); + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['HTTPS'] = null; + + $app = new App(); + $this->assertEquals(['example.com'], $app->allowedHostnames); + + // Clean up + putenv('ALLOWED_HOSTNAMES'); + + // Whitespace-only entry should be filtered + putenv('ALLOWED_HOSTNAMES=example.com, ,www.example.com'); + $_SERVER['HTTP_HOST'] = 'example.com'; + + $app = new App(); + $this->assertEquals(['example.com', 'www.example.com'], $app->allowedHostnames); + + // Clean up + putenv('ALLOWED_HOSTNAMES'); + } } \ No newline at end of file