Symlink Attack in Express with Hmac Signatures
Symlink Attack in Express with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A symlink attack in an Express application that uses Hmac signatures occurs when an attacker leverages file system trust boundaries to cause the server to sign or serve content it should not. This typically happens when user-influenced paths are used to locate files that are then read and included in responses, such as static assets or configuration files, while the server also uses Hmac signatures to authorize or verify those resources.
Consider an Express route that serves a file based on a user-supplied identifier and validates integrity using an Hmac. If the identifier is resolved to a file path without canonicalization and the application follows symbolic links, an attacker can place a symlink that points to a sensitive file outside the intended directory. When the server reads the file to compute or verify the Hmac, it may inadvertently process or expose data that should remain protected. For example, an attacker could trick the server into signing or returning contents of /etc/passwd or application secrets if the path traversal is possible through symlink resolution.
In this context, the Hmac signature does not protect against unauthorized file access because the signature is computed over data the attacker can influence indirectly through the filesystem. The vulnerability is not in the Hmac algorithm itself, but in the way file paths are resolved before the Hmac is applied. If the server uses a predictable naming scheme and symlinks are allowed in the document root or temporary directories, an attacker can create a symlink with a name that results in a valid Hmac signature for a malicious file. This can lead to data exposure or unauthorized actions being authenticated as legitimate by the server.
Real-world attack patterns such as CVE-2022-24999-style path traversal and symlink races highlight the risk when file system operations are not strictly confined. Even when Express middleware enforces some level of path validation, permissive symlink handling can bypass intended directory restrictions. The server’s trust in the filesystem location, combined with the assumption that an Hmac signature guarantees authenticity, creates a scenario where an attacker can manipulate which file is signed or returned, undermining the integrity guarantees the signature is meant to provide.
Hmac Signatures-Specific Remediation in Express — concrete code fixes
To mitigate symlink risks in Express when using Hmac signatures, ensure that file paths are resolved to their canonical form before any I/O or signing operation. Use Node.js built-in methods to eliminate symlinks and prevent directory traversal. Below are concrete, working examples that demonstrate secure handling of files with Hmac verification in Express.
Secure file serving with canonical paths and Hmac verification
const express = require('express');
const fs = require('fs').promises;
const crypto = require('crypto');
const path = require('path');
const app = express();
const SECRET = process.env.HMAC_SECRET || 'replace-with-strong-secret';
function computeHmac(filePath) {
const hmac = crypto.createHmac('sha256', SECRET);
hmac.update(filePath);
return hmac.digest('hex');
}
async function serveFileSafe(req, res) {
// User input should be treated as untrusted
const requested = req.query.file;
if (!requested) {
return res.status(400).send('Missing file parameter');
}
// Build a path confined to a specific directory
const baseDir = path.resolve(__dirname, 'public');
const unsafePath = path.join(baseDir, requested);
// Canonicalize: resolve symlinks and normalize
let canonicalPath;
try {
canonicalPath = path.resolve(unsafePath);
} catch (err) {
return res.status(400).send('Invalid path');
}
// Ensure the canonical path is still inside the allowed directory
if (!canonicalPath.startsWith(baseDir)) {
return res.status(403).send('Path traversal detected');
}
// Verify no symlinks remain within the resolved path
const normalized = path.normalize(canonicalPath);
if (normalized !== canonicalPath) {
return res.status(403).send('Path contains unsafe segments');
}
// Perform filesystem operations only after validation
try {
const data = await fs.readFile(canonicalPath);
const expectedSignature = computeHmac(canonicalPath);
const receivedSignature = req.query.signature;
// Constant-time comparison to avoid timing attacks
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature || '', 'hex')
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
res.set('Content-Type', 'text/plain');
res.send(data);
} catch (err) {
if (err.code === 'ENOENT') {
return res.status(404).send('File not found');
}
res.status(500).send('Server error');
}
}
app.get('/files', serveFileSafe);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Key remediation practices
- Always resolve paths with
path.resolve()and validate against a strict base directory. - Use
fs.stator similar checks if you need to explicitly reject symlinks (e.g.,stat(path).then(stats => stats.isSymbolicLink())). - Apply constant-time comparisons (e.g.,
crypto.timingSafeEqual) when verifying Hmac signatures to prevent timing attacks. - Avoid serving files directly based on user input; prefer serving files by mapped identifiers or IDs that are translated server-side to filesystem locations.
By combining strict path canonicalization with robust Hmac verification, you reduce the risk that symlink-based attacks can bypass authentication or integrity checks in Express applications.