HIGH symlink attackexpress

Symlink Attack in Express

How Symlink Attack Manifests in Express

Symlink attacks in Express applications typically occur when user-controlled input is used to construct file paths for serving static content or reading files. The core vulnerability arises when res.sendFile(), fs.readFile(), or express.static() middleware processes paths containing symbolic links that resolve to sensitive locations outside the intended directory.

The most common Express-specific manifestation involves serving user-uploaded content. Consider an endpoint that serves profile images:

app.get('/profile/:username/avatar', (req, res) => {
  const username = req.params.username;
  const filePath = `./uploads/${username}/avatar.jpg`;
  res.sendFile(filePath);
});

An attacker can exploit this by creating a directory with a symlink pointing to sensitive locations:

mkdir -p uploads/attacker
ln -s /etc/passwd uploads/attacker/avatar.jpg

When requesting /profile/attacker/avatar, Express resolves the symlink and exposes /etc/passwd. The attack succeeds because Express's sendFile doesn't validate whether the resolved path stays within the intended directory.

Another Express-specific scenario involves dynamic route parameters for file downloads:

app.get('/download/:file', (req, res) => {
  const file = req.params.file;
  res.download(`./files/${file}`);
});

Attackers can use ../ path traversal combined with symlinks to escape the intended directory. If ./files contains a symlink to /var/www, requesting /download/../../../etc/passwd could expose system files.

Express's express.static() middleware presents additional risks when serving user-generated content. If the static directory contains symlinks to system directories, clients can access files outside the intended scope:

app.use('/static', express.static('./public'));

If ./public contains config -> /etc/config, users can access /static/config/app.conf.

Express-Specific Detection

Detecting symlink attacks in Express requires both static analysis of code patterns and runtime scanning of deployed endpoints. middleBrick's Express-specific scanning identifies these vulnerabilities by testing unauthenticated endpoints with controlled inputs designed to trigger symlink resolution.

The scanner tests for common Express patterns by sending requests containing path traversal sequences and symlink indicators. For the profile avatar example, middleBrick would attempt:

GET /profile/attacker/avatar HTTP/1.1
Host: example.com

with controlled directory structures on the target server. The scanner checks if the response contains content from unexpected locations.

For download endpoints, middleBrick tests with sequences like:

GET /download/../../../etc/passwd HTTP/1.1
Host: example.com

The scanner also verifies if Express's built-in protections are properly configured. Express 4.17+ includes basic path traversal protection, but this can be bypassed when symlinks are involved.

middleBrick's scanning specifically tests Express middleware configurations. For express.static(), the scanner attempts to access files using encoded characters and path traversal attempts to verify if the middleware properly restricts access to the intended directory.

The scanner's LLM security module also checks for AI-specific attack vectors where symlinks might be used to exfiltrate model weights or training data through Express endpoints serving AI-generated content.

Code analysis tools can complement runtime scanning by identifying risky patterns:

const riskyPatterns = [
  /res\.sendFile\(['"`](.*?)['"`]/g,
  /fs\.readFile\(['"`](.*?)['"`]/g,
  /express\.static\(['"`](.*?)['"`]/g
];

These patterns help identify locations where symlink attacks could occur, though runtime scanning is essential since static analysis cannot determine if symlinks exist at deployment time.

Express-Specific Remediation

Express provides several native approaches to prevent symlink attacks. The most robust solution involves path validation before serving files. Implement a middleware that validates resolved paths:

const path = require('path');
const fs = require('fs');

function safeSendFile(res, filePath, options) {
  const resolvedPath = path.resolve(filePath);
  const baseDir = path.resolve('./uploads');
  
  if (!resolvedPath.startsWith(baseDir)) {
    return res.status(403).send('Forbidden');
  }
  
  res.sendFile(resolvedPath, options);
}

app.get('/profile/:username/avatar', (req, res) => {
  const username = req.params.username;
  const filePath = path.join('./uploads', username, 'avatar.jpg');
  safeSendFile(res, filePath);
});

This approach ensures the resolved path always stays within the intended directory, preventing symlink escapes.

For download endpoints, use similar validation:

app.get('/download/:file', (req, res) => {
  const file = req.params.file;
  const filePath = path.join('./files', file);
  const resolvedPath = path.resolve(filePath);
  const baseDir = path.resolve('./files');
  
  if (!resolvedPath.startsWith(baseDir)) {
    return res.status(403).send('Forbidden');
  }
  
  res.download(resolvedPath);
});

Express's sendFile and download methods accept an root option that helps restrict paths:

app.get('/profile/:username/avatar', (req, res) => {
  const username = req.params.username;
  const filePath = path.join(username, 'avatar.jpg');
  
  res.sendFile(filePath, {
    root: './uploads',
    dotfiles: 'deny'
  });
});

The dotfiles: 'deny' option prevents access to hidden files that might be used in symlink attacks.

For static file serving, use dotfiles: 'deny' and consider using fallthrough: false to prevent information leakage:

app.use('/static', express.static('./public', {
  dotfiles: 'deny',
  fallthrough: false
}));

// Additional validation for symlinks
app.use('/static', (req, res, next) => {
  const filePath = path.join('./public', req.path);
  const resolvedPath = path.resolve(filePath);
  const baseDir = path.resolve('./public');
  
  if (!resolvedPath.startsWith(baseDir)) {
    return res.status(403).send('Forbidden');
  }
  
  fs.stat(resolvedPath, (err, stats) => {
    if (err || stats.isSymbolicLink()) {
      return res.status(403).send('Forbidden');
    }
    next();
  });
});

This middleware checks if the resolved path is a symbolic link and blocks access, providing defense-in-depth against symlink attacks.

Frequently Asked Questions

How does middleBrick detect symlink attacks in Express applications?
middleBrick scans Express endpoints by sending requests with path traversal sequences and symlink indicators to test if the server resolves paths outside intended directories. The scanner checks for common Express patterns like res.sendFile(), res.download(), and express.static() configurations, attempting to access sensitive files through symlink resolution. It provides a security risk score with specific findings about symlink vulnerabilities and remediation guidance.
Can symlink attacks be completely prevented in Express?
While no defense is absolute, Express applications can be made highly resistant to symlink attacks through proper path validation, using the root option with sendFile/download, denying dotfile access, and implementing middleware that checks for symbolic links before serving files. The key is ensuring that resolved paths always stay within intended directories and that symbolic links are explicitly blocked or handled securely.