Express.js PhantomJS Command Injection

Critical Risk Command Injection
expressphantomjscommand-injectionjavascriptcode-executionpdf-generation

What it is

The Express.js application uses PhantomJS for server-side rendering or PDF generation with user-controlled input, leading to command injection vulnerabilities. Attackers can inject malicious commands that get executed by the PhantomJS process, potentially leading to remote code execution.

// Vulnerable: Direct user input to PhantomJS
const phantom = require('phantom');

app.post('/generate-pdf', async (req, res) => {
  const url = req.body.url;
  const instance = await phantom.create();
  const page = await instance.createPage();
  
  // Dangerous: user-controlled URL without validation
  await page.open(url);
  await page.render('/tmp/output.pdf');
  
  res.sendFile('/tmp/output.pdf');
});
// Secure: Input validation and sanitization
const phantom = require('phantom');
const validator = require('validator');
const { URL } = require('url');

const ALLOWED_DOMAINS = ['example.com', 'trusted-site.com'];

app.post('/generate-pdf', async (req, res) => {
  const urlString = req.body.url;
  
  try {
    // Validate URL format
    const url = new URL(urlString);
    
    // Check against allowlist
    if (!ALLOWED_DOMAINS.includes(url.hostname)) {
      return res.status(400).json({ error: 'Domain not allowed' });
    }
    
    const instance = await phantom.create();
    const page = await instance.createPage();
    
    await page.open(url.toString());
    await page.render('/tmp/output.pdf');
    
    res.sendFile('/tmp/output.pdf');
  } catch (error) {
    res.status(400).json({ error: 'Invalid URL' });
  }
});

💡 Why This Fix Works

The vulnerable code was updated to address the security issue.

Why it happens

Express applications construct PhantomJS command-line invocations using child_process.exec() or spawn() with user-controlled input directly in arguments. Code concatenates user input into PhantomJS commands: exec('phantomjs render.js ' + userUrl) or spawn('phantomjs', ['script.js', req.query.url]). Attackers inject shell metacharacters (semicolons, pipes, backticks) or PhantomJS-specific arguments to execute arbitrary commands. PhantomJS command injection allows attackers to read files using --local-to-remote-url-access=yes flag, execute JavaScript through injected page URLs, or chain with other PhantomJS options for code execution.

Root causes

User Input Passed Directly to PhantomJS Command Arguments

Express applications construct PhantomJS command-line invocations using child_process.exec() or spawn() with user-controlled input directly in arguments. Code concatenates user input into PhantomJS commands: exec('phantomjs render.js ' + userUrl) or spawn('phantomjs', ['script.js', req.query.url]). Attackers inject shell metacharacters (semicolons, pipes, backticks) or PhantomJS-specific arguments to execute arbitrary commands. PhantomJS command injection allows attackers to read files using --local-to-remote-url-access=yes flag, execute JavaScript through injected page URLs, or chain with other PhantomJS options for code execution.

Insufficient Sanitization of URLs and HTML Content

Applications pass user-provided URLs or HTML content to PhantomJS for rendering, PDF generation, or screenshot capture without proper validation. PhantomJS processes URLs like file:///etc/passwd, javascript: URLs executing code, or data: URLs with embedded scripts. User-supplied HTML contains <script> tags, event handlers, or PhantomJS-specific hooks that execute when rendered. Applications don't validate URL schemes, don't check for local file access attempts, and don't sanitize HTML before passing to PhantomJS. PhantomJS executes JavaScript in rendered pages with access to local file system through its security model, enabling data exfiltration.

User Input in PhantomJS Script Parameters

PhantomJS scripts receive user input through system.args or webpage.open() calls without validation. Express applications pass user data to PhantomJS scripts: phantomjs render.js --url=${userInput} --format=${userFormat}. Scripts directly use these parameters in filesystem operations, network requests, or eval() calls: page.open(system.args[1]). Attackers provide malicious arguments that exploit script logic, inject PhantomJS API calls, or manipulate script execution flow. Scripts assume input is trustworthy without implementing parameter validation, type checking, or sanitization within PhantomJS execution context.

Missing File Path Validation for PhantomJS Operations

Applications use user input to determine output file paths for PhantomJS-generated PDFs, screenshots, or rendered HTML without proper path validation. Code constructs file paths from user input: page.render('/var/www/output/' + req.query.filename + '.pdf'). Path traversal attacks using ../ sequences allow writing files outside intended directories. Attackers overwrite sensitive files like configuration, execute code by writing to web-accessible directories, or exploit race conditions in file operations. PhantomJS file operations don't enforce sandbox restrictions on output paths, executing with full process permissions.

Dynamic PhantomJS Script Generation with User Content

Applications dynamically generate PhantomJS scripts by concatenating user input into JavaScript code executed by PhantomJS. Code creates temporary script files containing user data: const script = `page.open('${userUrl}', function() {...})`; fs.writeFileSync('/tmp/script.js', script). User input breaks out of string context to inject arbitrary JavaScript, manipulate PhantomJS API calls, or access restricted resources. Dynamic script generation enables full PhantomJS environment compromise as injected code executes with same privileges as legitimate script. Attackers leverage PhantomJS filesystem module, system module, or webpage module for malicious operations.

Fixes

1

Validate and Sanitize All User Input Before PhantomJS

Implement comprehensive input validation and sanitization before any PhantomJS operations. Validate URLs using Node.js URL class: const url = new URL(userInput); if (!['http:', 'https:'].includes(url.protocol)) reject. Check URL hostnames against allowlist of permitted domains. Sanitize HTML content using DOMPurify or sanitize-html libraries to remove scripts and dangerous elements. Validate file paths to ensure they're within expected directories using path.resolve() and checking for path traversal. Use parameterized execution with child_process.spawn() passing arguments as array elements instead of concatenating command strings. Reject any input containing shell metacharacters or PhantomJS-specific flags.

2

Implement Strict URL and File Path Allowlists

Create explicit allowlists of permitted domains, URL patterns, and file paths for PhantomJS operations. Define allowed URL domains: const allowedDomains = ['trusted-site.com', 'cdn.example.com']; validate user URLs match these domains. For file operations, restrict output paths to specific directories: if (!outputPath.startsWith('/var/app/renders/')) reject. Use indirect references where users select from predefined options rather than providing URLs directly: map user selection IDs to server-controlled URLs. Maintain allowlists in configuration files separate from application code. Log all rejected attempts to detect attack patterns. Never construct file paths or URLs through string concatenation with user input.

3

Enforce Strict Parameter Validation in PhantomJS Scripts

Implement validation logic within PhantomJS scripts to verify all parameters received from command line or Express application. Check system.args array for expected types, formats, and values: if (!/^https?:\/\//.test(system.args[1])) phantom.exit(1). Validate parameters against expected patterns before using in PhantomJS API calls. Use try-catch blocks around PhantomJS operations to handle invalid input gracefully. Never pass user input directly to eval(), phantom.evaluate(), or page.evaluate() functions. Implement timeout limits on PhantomJS operations to prevent hung processes. Log validation failures within PhantomJS scripts for security monitoring.

4

Execute PhantomJS in Sandboxed Environments

Run PhantomJS processes in containerized or sandboxed environments with restricted permissions. Use Docker containers with minimal capabilities, read-only root filesystem, and no-new-privileges flag. Configure AppArmor or SELinux profiles limiting PhantomJS file system and network access. Run PhantomJS under dedicated user account with minimal permissions, not as root or application user. Use chroot jails or systemd sandboxing to restrict PhantomJS visibility of filesystem. Implement resource limits (CPU, memory, process count) using cgroups or ulimit to contain DoS attacks. Monitor PhantomJS process behavior for anomalous file access, network connections, or resource usage indicating exploitation attempts.

5

Migrate to Puppeteer with Comprehensive Security Controls

Replace PhantomJS (abandoned project) with Puppeteer or Playwright which have better security models and active maintenance. Configure Puppeteer with security options: {args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], headless: true}. Use Puppeteer's JavaScript evaluation sandbox: page.evaluate() runs in isolated context without Node.js access. Implement navigation timeouts to prevent hung browsers: page.goto(url, {timeout: 30000, waitUntil: 'networkidle0'}). Validate URLs before navigation, disable JavaScript for untrusted content when possible, and use Content Security Policy. Puppeteer provides better resource management, modern Chrome security features, and active security patch support compared to abandoned PhantomJS.

6

Never Generate Dynamic Scripts with User Content

Eliminate all dynamic PhantomJS script generation using user input. Use static, pre-written PhantomJS scripts that accept parameters through validated command-line arguments only. If customization is required, use configuration-based approaches: pass JSON configuration file path to script rather than generating code. For template-based rendering, use data-driven templates where user input is data only, never code. Remove any code that writes PhantomJS scripts to temporary files with user content: delete fs.writeFileSync('/tmp/script.js', userTemplate). Pre-compile and validate all PhantomJS scripts during application deployment. If absolutely necessary to customize behavior, use allowlisted function dispatch pattern rather than dynamic code generation.

Detect This Vulnerability in Your Code

Sourcery automatically identifies express.js phantomjs command injection and many other security issues in your codebase.