HIGH container escapeexpress

Container Escape in Express

How Container Escape Manifests in Express

Container escape vulnerabilities in Express applications typically arise when the Node.js runtime inside a container can access privileged resources or break out of its isolation boundaries. Unlike traditional web application vulnerabilities, container escapes exploit the gap between the containerized application and the host operating system.

One common pattern involves Express applications that mount sensitive host directories. Consider an Express app that serves static files from a mounted volume:

const express = require('express');
const app = express();

// Dangerous: mounting host filesystem
app.use('/host', express.static('/host'));

app.listen(3000, () => console.log('Listening on port 3000'));

If an attacker can control the path parameter, they might traverse to sensitive files outside the intended directory. Express's path.join() can be exploited for directory traversal:

app.get('/download/:filename', (req, res) => {
const filePath = path.join('/safe/directory', req.params.filename);
// path.join('/safe/directory', '../../etc/passwd') => '/etc/passwd'
res.download(filePath);
});

Another Express-specific vector involves the child_process module. Express route handlers that spawn processes with user-controlled input can break container isolation:

app.post('/exec', (req, res) => {
const { command } = req.body;
// Allows container escape via privileged commands
const exec = require('child_process').exec;
exec(command, (err, stdout) => res.send(stdout));
});

Express applications running as root inside containers (common with port 80/443 bindings) can modify host system files if the container's filesystem is not properly isolated. The fs module combined with improper validation creates escape opportunities:

app.post('/write', (req, res) => {
const { path: filePath, content } = req.body;
fs.writeFileSync(filePath, content); // No validation!
res.send('File written');
});

Process enumeration via ps commands from Express routes can reveal host processes, especially in shared hosting environments where containers share kernel namespaces. An Express endpoint like:

app.get('/processes', (req, res) => {
require('child_process').exec('ps aux', (err, stdout) => {
res.send(stdout); // Exposes host process information
});
});

Network-based escapes occur when Express applications bind to all interfaces (0.0.0.0) and expose administrative endpoints. If the container network is misconfigured, these endpoints might be accessible from the host network, bypassing container network policies.

Express-Specific Detection

Detecting container escape vulnerabilities in Express requires examining both the application code and runtime configuration. Static analysis of Express route handlers should flag dangerous patterns:

// Dangerous patterns to scan for:
const dangerousPatterns = [
/exec\(.*\)/, // exec() calls
/spawn\(.*\)/, // spawn() calls
/fs\.writeFile/, // file writes
/app\.use.*static.*\//, // static mounts with absolute paths

Dynamic scanning with middleBrick can identify runtime container escape vectors. The scanner tests Express endpoints for directory traversal by sending path traversal payloads:

const traversalTests = [
'../',
'../../',
'../etc/passwd',
'../proc/self/environ',

middleBrick's black-box scanning specifically targets Express applications by testing common Express patterns. It sends requests to typical Express endpoints and analyzes responses for signs of privilege escalation or filesystem access. The scanner checks for:

  • Path traversal in Express route parameters and query strings
  • Command injection in Express request bodies
  • Static file serving that might expose host files
  • Process information disclosure through Express endpoints

For containerized Express apps, middleBrick examines Docker-specific configurations. It checks for privileged containers, mounted volumes, and exposed ports that could facilitate escapes. The scanner tests whether the Express application running inside the container can access host resources by attempting to read from /proc, /etc, and other sensitive locations.

Network-based detection involves scanning Express endpoints listening on all interfaces. middleBrick identifies Express applications that bind to 0.0.0.0 and tests whether these can be accessed from outside the container network, potentially exposing administrative interfaces.

Express-Specific Remediation

Securing Express applications against container escape requires defense in depth. Start with proper input validation using Express middleware:

const express = require('express');
const path = require('path');
const app = express();

// Input sanitization middleware
app.use(express.urlencoded({ extended: false }));
app.use(express.json());

// Safe file serving with path validation
function safeFilePath(baseDir, userPath) {
const resolved = path.resolve(baseDir, userPath);
if (!resolved.startsWith(baseDir)) {
throw new Error('Path traversal attempt detected');
}
return resolved;
}

app.get('/download/:filename', (req, res) => {
try {
const safePath = safeFilePath('/safe/directory', req.params.filename);
res.download(safePath);
} catch (err) {
res.status(400).send('Invalid file path');
}
});

For process execution, use strict whitelisting and avoid shell invocation:

app.post('/exec', (req, res) => {
const { command, args } = req.body;

if (!allowedCommands.includes(command)) {
return res.status(403).send('Command not allowed');
}

const { spawn } = require('child_process');
const child = spawn(command, args, { shell: false });

let output = '';
child.stdout.on('data', data => output += data);
child.on('close', () => res.send(output));
});

Container-specific security for Express involves running as non-root user and using Node.js's built-in security features:

// Dockerfile example
FROM node:18-alpine

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001

# Switch to non-root user
USER nodejs

COPY --chown=nodejs:nodejs . .
EXPOSE 3000
CMD ["node", "server.js"]

Express middleware can enforce security headers and prevent common attack patterns:

const helmet = require('helmet');
app.use(helmet());

// Prevent directory listing
app.use(express.static('public', { index: false }));

// Rate limiting to prevent brute force
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests'
});
app.use(limiter);

For filesystem operations, use the fs.promises API with proper error handling and avoid synchronous operations in production:

const fs = require('fs').promises;

async function safeWrite(filePath, content) {
const resolved = path.resolve('/allowed/directory', filePath);
if (!resolved.startsWith('/allowed/directory')) {
throw new Error('Invalid path');
}
await fs.writeFile(resolved, content, { flag: 'w' });
}

Network security involves binding only to necessary interfaces and using firewall rules:

app.listen(3000, '127.0.0.1', () => {
console.log('Express app listening on localhost:3000');
});

Regular scanning with middleBrick helps identify new vulnerabilities as code changes. The GitHub Action integration can automatically scan Express APIs in CI/CD pipelines:

name: API Security Scan
on: [push, pull_request]

jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run middleBrick Scan
run: middlebrick scan https://your-express-app.com
- name: Fail on high severity issues
if: failure()

Frequently Asked Questions

How can I tell if my Express app is vulnerable to container escape?
Look for Express endpoints that accept file paths, execute commands, or expose system information. Check if your container runs as root, mounts host volumes, or binds to all network interfaces. Use middleBrick to scan your Express API endpoints - it tests for path traversal, command injection, and other container escape vectors specific to Express applications.
What's the difference between a container escape and a regular Express vulnerability?
Container escapes specifically break the isolation between your Express application and the host operating system. While regular Express vulnerabilities like SQL injection or XSS affect your application's data, container escapes can give attackers access to the entire host system, other containers, and network resources. Express-specific container escapes often involve file system access, process execution, or network binding misconfigurations.