# Patch Analysis: GHSA-jf2j-jhvf-rc56 / CVE-2026-23698 Vtiger CRM Module Import RCE

## Target and Version

- **Product**: Vtiger CRM
- **Version tested**: 8.4.0 (SourceForge release tarball `vtigercrm8.4.0.tar.gz`, md5 `360f394f5f7ffabc89eaca05b18523f9`)
- **Patch status**: No vendor patch is available. The CVE record lists the affected version range as "through 8.4.0" and the patched versions as unknown. The public disclosure (Jiva Security, 2026-07-06) confirms Vtiger did not ship a fix before the 90-day disclosure window expired.

## Threat Model and Security Policy

No `SECURITY.md` or public threat model for Vtiger CRM was found in the 8.4.0 tarball. The disclosure and advisory treat the issue as an authenticated admin-to-RCE vulnerability. All three import paths evaluated here (module, language, layout) are reachable only by an authenticated administrator through the **Module Manager → Import** UI. They cross the same trust boundary: a user who has already authenticated as an admin can cause the server to write executable PHP files into the web root.

## Vulnerability Summary

The original vulnerability is an **unrestricted upload of file with dangerous type** (CWE-434). The `ModuleManager::Import` feature accepts a ZIP archive, validates only that it contains a `manifest.xml` with a compatible Vtiger version, and extracts the contents directly under the web root. For a normal module package, this places files under `modules/<moduleName>/`, where Apache executes any `.php` file before Vtiger's front-controller routing can enforce authentication or authorization.

## Code Paths Involved

### 1. Original module import path

- **Upload handler**: `modules/Settings/ModuleManager/views/ModuleImport.php::importUserModuleStep2`
- **Install handler**: `modules/Settings/ModuleManager/actions/Basic.php::importUserModuleStep3`
- **Extraction sink**: `vtlib/Vtiger/PackageImport.php::initImport()` (via `Vtiger_Package::import()`)
- **Behavior**: `unzipAllEx` extracts `modules/<module>/`, `cron/modules/<module>/`, `layouts/...`, `languages/...`, etc., with no file-type or content validation.
- **Result**: Any PHP file placed in the ZIP under the module tree becomes reachable and executable via `/modules/<module>/<file>.php`.

### 2. Language pack import path

- **Install handler**: `modules/Settings/ModuleManager/actions/Basic.php::importUserModuleStep3` (dispatches to `new Vtiger_Language()` when `module_import_type == 'language'`)
- **Extraction sink**: `vtlib/Vtiger/LanguageImport.php::import_Language()`
- **Behavior**: Iterates the ZIP file list, accepts files under `modules/` (and `modules/Settings/`) and rewrites them into `languages/<prefix>/`. No content or extension validation is performed.
- **Result**: A PHP shell placed at `modules/shell.php` in the ZIP is written to `languages/<prefix>/shell.php` and can be executed directly via HTTP.

### 3. Layout pack import path

- **Install handler**: `modules/Settings/ModuleManager/actions/Basic.php::importUserModuleStep3` (dispatches to `new Vtiger_Layout()` when `module_import_type == 'layout'`)
- **Extraction sink**: `vtlib/Vtiger/LayoutImport.php::import_Layout()`
- **Behavior**: Extracts files under `layouts/<name>/skins/...`, `layouts/<name>/modules/...`, and `layouts/<name>/libraries/...`. Unlike the module and language paths, this sink contains **some** validation.

## What the Existing (Partial) Fix Does

`vtlib/Vtiger/LayoutImport.php` attempts to block PHP files before extraction:

1. `Vtiger_Functions::verifyClaimedMIME()` is called against the bad-extension list from `$upload_badext` (minus `js`). This compares a fileinfo MIME type such as `text/x-php` to an array of file extensions (e.g., `php`, `php3`, ...). Because the comparison is between MIME types and extension strings, the PHP MIME type is **not** in the extension list, so this check always passes for PHP files.
2. A regex content check is applied:
   ```php
   if (preg_match('/(<\?php?(.*?))/i', $imageContents) == 1) {
       $fileValidation = false;
   }
   ```
   This pattern matches the sequences `<?php`, `<?h`, `<?ph`, `<?hp`, and `<?` (with optional `p`/`h`/`p`). It does **not** match the PHP short-echo tag `<?=`, which is universally enabled in PHP 5.4+.

So the layout import has a partial defense-in-depth measure that is trivially bypassed by using `<?= ... ?>` instead of `<?php ... ?>`.

## What the Fix Does NOT Cover

A vendor patch has not been released, but even if one were applied narrowly to the original module import, the following gaps would remain unless the fix is comprehensive:

1. **Language import**: No file content or extension validation. Any PHP file under the `modules/` tree of a language ZIP is extracted into `languages/<prefix>/`.
2. **Layout import**: The PHP content filter is incomplete and bypassable with short-echo tags. The MIME check is also ineffective because it compares MIME types to extension strings.
3. **Other archive-based import paths**: The same `importUserModuleStep3` dispatcher handles module, language, and layout imports. Any future import type added through this dispatcher would inherit the same risk unless the fix is centralized.
4. **No relocation**: None of the three sinks stage archives outside the document root before extraction. All extracted files remain directly web-reachable.
5. **No extension allowlist**: The code does not restrict files to a safe list of extensions (e.g., `.php` only for known module class files, `.js`, `.css`, `.png`, `.tpl`, `.less`, `.lang.php` with safe content). As a result, arbitrary PHP files, including web shells, are accepted.

## Comparison of Behavior Before/After a Hypothetical Fix

A correct fix would:

- Centralize validation in `importUserModuleStep3` or in the `Vtiger_Unzip` layer before any file is written to disk.
- Maintain an allowlist of expected file extensions and relative paths per package type.
- Reject PHP files that are not legitimate, well-known module/language/layout source files (e.g., reject a bare `shell.php` in a module or language pack).
- Stage the extracted contents outside the web root, or serve them through a non-executable, authenticated front-controller endpoint.

Without such a fix, the behavior is identical across all three paths: an authenticated admin uploads a ZIP, the server extracts attacker-controlled PHP into the web root, and the PHP executes when accessed directly via HTTP.

## Recommendations for a Complete Fix

1. **Add a path/extension allowlist before extraction** in `Vtiger_PackageImport`, `Vtiger_LanguageImport`, and `Vtiger_LayoutImport`.
2. **Fix the layout content check** to use a robust PHP detection mechanism (e.g., tokenization with `token_get_all()`) instead of a bypassable regex. Alternatively, reject PHP files entirely in layout packs.
3. **Fix `verifyClaimedMIME`** to compare MIME types to MIME type allowlists, not to extension strings.
4. **Move the extraction target** outside the web root. Serve module/language/layout assets through a PHP download handler that enforces authentication and sets `Content-Disposition: attachment` for non-executable files.
5. **Add regression tests** for all three package types that assert a ZIP containing an arbitrary PHP shell is rejected and the PHP file is never created under `modules/`, `languages/`, or `layouts/`.
