Post

Code Review: Exploiting SSTI in Node.js Template Rendering

Code Review: Exploiting SSTI in Node.js Template Rendering

Overview

In this code review challenge on AppSecMaster, the task was to identify and exploit a vulnerability within a custom Node.js application. By carefully analyzing the code, I discovered a classic Server-Side Template Injection (SSTI) vulnerability caused by insecure usage of Node’s vm module. This allowed me to access sensitive server-side information, specifically environment variables.

Vulnerable Code Analysis

The core of the vulnerability lies in the following function:

1
2
3
4
5
6
7
8
9
10
11
12
function renderTemplate(template, data) {
  return template.replace(/\${(.+?)}/g, (match, expression) => {
    try {
      const sandbox = { ...data };
      const context = vm.createContext(sandbox);
      return vm.runInContext(expression, context);
    } catch (err) {
      console.error("Template error:", err);
      return `[Error evaluating: ${expression}]`;
    }
  });
}

This function is responsible for evaluating dynamic expressions embedded inside ${...} placeholders in the HTML templates. It uses the vm module to execute the content of these expressions in a sandboxed context.

The issue is that the data object passed to the rendering function includes process, as seen here:

1
2
const data = { process };
const html = renderTemplate(template, data);

This gives user-controlled expressions direct access to process, which exposes environment variables and other critical server-side objects.

Exploitation

The vulnerable entry point is the /send-message route, which processes user input and injects it into a template string:

1
2
3
4
5
6
7
const template = `
  ...
  <div class="message-content">
    ${message}
  </div>
  ...
`;

Since the message field is taken directly from the POST body and inserted into the template without sanitization, an attacker can inject arbitrary expressions.

To exploit this, I submitted the following payload:

1
message=${process.env.MASTER_KEY}

This caused the template renderer to evaluate process.env.MASTER_KEY and output its value directly into the resulting HTML page.

Impact

This vulnerability leads to arbitrary code execution in a restricted context, which can be escalated to information disclosure or even Remote Code Execution (RCE) under certain conditions. In this specific case, it allowed direct access to secret environment variables, including MASTER_KEY, which could be used for further privilege escalation or data breaches.

Recommendations

To prevent such vulnerabilities:

  • Avoid dynamic code execution like vm.runInContext() with user input.

  • Use secure and well-maintained templating engines such as EJS, Handlebars, or Pug, which provide built-in output escaping.

  • Never pass sensitive objects like process, require, or Function into a user-controlled execution context.

  • Sanitize and validate all user input rigorously.

Conclusion

This challenge demonstrated the risks of building custom template renderers using unsafe methods such as vm.runInContext. A seemingly harmless feature—dynamic rendering of user input—became a vector for serious server-side injection. Always treat user input as untrusted, and rely on hardened libraries rather than reinventing core components.


thanks for reading. If you enjoyed this write-up, feel free to follow me on Twitter

This post is licensed under CC BY 4.0 by the author.