Symlink Attack in Express with Dynamodb
Symlink Attack in Express with Dynamodb — how this specific combination creates or exposes the vulnerability
A symlink attack in an Express service that uses DynamoDB typically arises when the application builds file paths for user-controlled values and stores or references those paths in DynamoDB records. If an attacker can control the filename or key used to construct a path, they may provide a specially crafted name such as ../../../etc/passwd or a symbolic link segment that traverses directories. When the application later reads from or writes to that path, the symlink can redirect operations outside the intended directory, leading to unauthorized read or write access. Storing these manipulated paths or keys in DynamoDB means that the persistence layer records the malicious reference, allowing the attack to survive deployments or configuration changes.
In a typical Express + DynamoDB flow, an endpoint might accept a user identifier and a filename, then construct a path such as ./uploads/${userId}/${filename}. If filename contains directory traversal or a symlink name, and the application does not validate or sanitize it, the resolved path can point to sensitive locations. DynamoDB stores metadata such as the userId and filename, which an attacker can leverage to read or overwrite arbitrary files when the application resolves those paths later. Because the scan category Property Authorization tests whether access checks are consistently enforced, an insecure implementation may be flagged for allowing path manipulation across authorization boundaries.
Because middleBrick scans the unauthenticated attack surface, it can detect indicators such as missing validation of user-supplied path components and insecure usage patterns that enable symlink-based attacks. Findings highlight the absence of canonicalization and path normalization before filesystem operations, insufficient authorization checks on file access, and lack of input validation that would prevent traversal sequences or malicious filenames from being stored in DynamoDB.
Dynamodb-Specific Remediation in Express — concrete code fixes
Remediation focuses on strict input validation, path normalization, and avoiding direct concatenation of user input into filesystem paths. Use a canonicalization step before any file operation, and ensure that authorization checks are applied consistently for both metadata stored in DynamoDB and the resulting filesystem access.
Validation and canonicalization example
const path = require('path');
const crypto = require('crypto');
function safeFilename(input) {
// Reject path separators, null bytes, and other problematic characters
if (typeof input !== 'string' || /[\\/\0]/.test(input)) {
throw new Error('Invalid filename');
}
// Remove sequences like '..' and resolve to a safe basename
const base = path.basename(input);
const safe = path.normalize(base).replace(/^(\.\.[\/\\])+/, '');
if (safe !== base || !/^[a-zA-Z0-9._-]+$/.test(safe)) {
throw new Error('Invalid filename');
}
return safe;
}
function userIdKey(userId) {
// Ensure userId is alphanumeric to avoid traversal in key design
if (!/^[a-zA-Z0-9_-]+$/.test(userId)) {
throw new Error('Invalid userId');
}
return `user/${userId}`;
}
Express route with DynamoDB and safe file handling
const express = require('express');
const { DynamoDBClient, GetCommand, PutCommand } = require('@aws-sdk/client-dynamodb');
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const router = express.Router();
const ddb = new DynamoDBClient({});
const s3 = new S3Client({});
// Store metadata in DynamoDB and file in S3
router.post('/upload', async (req, res) => {
try {
const cleanName = safeFilename(req.body.filename);
const key = `${userIdKey(req.body.userId)}/${cleanName}`;
// Record intent in DynamoDB (metadata only, no filesystem path)
const putCmd = new PutCommand({
TableName: process.env.METADATA_TABLE,
Item: {
userId: { S: req.body.userId },
filename: { S: cleanName },
key: { S: key },
uploadedAt: { S: new Date().toISOString() }
}
});
await ddb.send(putCmd);
// Store actual content in S3 (or your storage); avoid local disk writes for user content
const uploadCmd = new PutObjectCommand({
Bucket: process.env.FILE_BUCKET,
Key: key,
Body: req.file.buffer,
ContentType: req.file.mimetype
});
await s3.send(uploadCmd);
res.status(201).json({ message: 'uploaded', key });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// Retrieve using canonical key from DynamoDB metadata
router.get('/file/:userId/:filename', async (req, res) => {
try {
const cleanName = safeFilename(req.params.filename);
const key = `${userIdKey(req.params.userId)}/${cleanName}`;
const cmd = new GetCommand({
TableName: process.env.METADATA_TABLE,
Key: {
userId: { S: req.params.userId },
filename: { S: cleanName }
}
});
const meta = await ddb.send(cmd);
if (!meta.Item) {
return res.status(404).json({ error: 'not found' });
}
const getCmd = new GetObjectCommand({
Bucket: process.env.FILE_BUCKET,
Key: key
});
const obj = await s3.send(getCmd);
res.set('Content-Type', obj.ContentType);
obj.Body.pipe(res);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
Why this approach mitigates symlink risks
- Input validation: Rejects path separators and traversal sequences before any path building occurs.
- Canonicalization and basename extraction: Ensures the final filename cannot escape intended directories.
- Separation of metadata and storage: DynamoDB stores only canonical metadata; actual content lives in a service like S3 where path traversal is not an issue.
- Consistent authorization: Access checks use the same canonical identifiers stored in DynamoDB, preventing mismatches between metadata and filesystem operations.
If you use local disk storage, apply the same validation and store files below a dedicated, non-traversable root, using resolved absolute paths for access. middleBrick can help identify missing validation and inconsistent authorization in your scan reports, guiding you toward safer design patterns.