Container Escape in Sails
How Container Escape Manifests in Sails
Container escape in Sails applications typically occurs through improper handling of file system operations and path traversal vulnerabilities. When Sails applications need to read configuration files, access user uploads, or serve static assets, they often construct file paths dynamically. If these paths aren't properly sanitized, an attacker can manipulate input to break out of the intended directory structure.
A common pattern in Sails involves using require() or fs.readFile() with user-controlled paths. For example, an API endpoint that reads configuration files based on a filename parameter might look like:
module.exports = {
config: function (req, res) {
const filename = req.query.filename || 'default.json';
const configPath = path.join(__dirname, '../config/', filename);
const config = require(configPath);
res.json(config);
}
};The vulnerability here is that path.join() doesn't prevent path traversal. An attacker can request /api/config?filename=../../package.json to read files outside the intended directory. More sophisticated attacks use URL encoding like ..%2F..%2F to bypass simple filters.
Sails's Waterline ORM can also be exploited for container escape when combined with improper file handling. Consider an application that stores file paths in the database and later reads them:
module.exports = {
download: function (req, res) {
Upload.findOne(req.params.id).exec(function (err, upload) {
if (err || !upload) return res.notFound();
const filePath = upload.path; // User-controlled path from DB
res.download(filePath);
});
}
};If an attacker can influence the upload.path field through SQL injection or direct database manipulation, they can cause the application to read arbitrary files. This becomes a container escape when the application runs with elevated privileges or when the container's file system contains sensitive files.
Another Sails-specific scenario involves policy-based file serving. Sails policies might allow authenticated users to access certain directories, but if the policy doesn't properly validate the resolved path, traversal is possible:
module.exports = {
allowed: function (req, res, next) {
if (!req.user) return res.forbidden();
const filePath = path.join('/var/app/uploads', req.query.file);
// Missing path validation here!
req.filePath = filePath;
next();
}
};Even with authentication, this allows any authenticated user to read files anywhere on the container's file system that the application process can access.
Sails-Specific Detection
Detecting container escape vulnerabilities in Sails requires examining both code patterns and runtime behavior. Static analysis should focus on finding instances where user input influences file system operations without proper validation.
Code review should look for these specific patterns in Sails controllers and policies:
// Dangerous patterns to flag:
const filePath = path.join(baseDir, req.query.path);
const content = fs.readFileSync(filePath, 'utf8');
// No validation that filePath starts with baseDir
const filePath = path.resolve(req.query.path);
// path.resolve() can create absolute paths, bypassing containment
const filePath = __dirname + '/' + req.query.file;
// String concatenation is especially dangerous
middleBrick's API security scanner can detect these vulnerabilities by testing endpoints with path traversal payloads. For a Sails application, middleBrick would send requests like:
curl -X GET "http://your-sails-app.com/api/config?filename=../../package.json"
curl -X GET "http://your-sails-app.com/api/download?file=../../../../etc/passwd"
The scanner checks if the application returns file contents rather than proper error responses. middleBrick's black-box approach means it doesn't need access to your source code—it tests the actual runtime behavior of your deployed API endpoints.
For OpenAPI spec analysis, middleBrick examines your Swagger/OpenAPI definitions to identify endpoints that accept file paths or filenames as parameters. It then correlates this with its runtime scanning results to provide comprehensive coverage. The scanner tests 12 security categories including path traversal (related to container escape), authentication bypass, and data exposure.
Runtime detection should also include monitoring for unusual file access patterns. In a containerized environment, you can use auditd or similar tools to log file access attempts. Look for processes accessing files outside their expected directories, especially attempts to read /etc/passwd, /proc, or other system files.
Sails-Specific Remediation
Remediating container escape vulnerabilities in Sails requires a defense-in-depth approach. Start with proper path validation using Node.js's built-in path module:
const validatePath = (basePath, relativePath) => {
const resolvedPath = path.resolve(basePath, relativePath);
if (!resolvedPath.startsWith(basePath)) {
throw new Error('Path traversal attempt detected');
}
return resolvedPath;
};
module.exports = {
config: function (req, res) {
try {
const filename = req.query.filename || 'default.json';
const configPath = validatePath(
path.join(__dirname, '../config/'),
filename
);
const config = require(configPath);
res.json(config);
} catch (err) {
return res.status(400).json({ error: 'Invalid file request' });
}
}
};For file serving, use Sails's built-in res.download() with validated paths, or better yet, use a whitelist approach:
const allowedFiles = ['config.json', 'settings.json', 'default.json'];
module.exports = {
config: function (req, res) {
const filename = req.query.filename || 'default.json';
if (!allowedFiles.includes(filename)) {
return res.status(400).json({ error: 'File not allowed' });
}
const configPath = path.join(__dirname, '../config/', filename);
try {
const config = require(configPath);
res.json(config);
} catch (err) {
return res.status(500).json({ error: 'File read error' });
}
}
};For database-driven file paths, implement strict validation before accessing the file system:
module.exports = {
download: function (req, res) {
Upload.findOne(req.params.id).exec(function (err, upload) {
if (err || !upload) return res.notFound();
const uploadDir = '/var/app/uploads/';
const filePath = validatePath(uploadDir, upload.path);
res.download(filePath, function (err) {
if (err) {
return res.status(404).json({ error: 'File not found' });
}
});
});
}
};Container-level hardening is also essential. Run your Sails application with the least privileges necessary, use read-only file systems where possible, and implement AppArmor or SELinux profiles to restrict file system access. In your Dockerfile, avoid running as root and limit mounted volumes to only what's necessary:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs
EXPOSE 1337
CMD ["node", "server.js"]Finally, implement comprehensive logging and monitoring. Log all file access attempts, especially those that fail validation, and set up alerts for suspicious patterns like repeated attempts to access system files or traverse directories.
Frequently Asked Questions
How can I test if my Sails application is vulnerable to container escape?
../ sequences in file path parameters and checking if the server returns unexpected file contents.