GitHub Security Advisory: GHSA-8rh5-4mvx-xj7j
Package: ci4-cms-erp/ci4ms (COMPOSER)
Severity: HIGH (CVSS 8.1)
CWE: CWE-306: Missing Authentication for Critical Function
Vulnerable versions: <= 0.31.3.0
Patched in: 0.31.4.0

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

Details:
## 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:

```php
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:

```php
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`:

```php
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 tempo