Crlf Injection in Sails
How Crlf Injection Manifests in Sails
CRLF injection in Sails applications occurs when untrusted user input containing carriage return ( ) and line feed ( ) characters is incorporated into HTTP headers without proper sanitization. Sails, built on Express.js, inherits Express's header processing behavior, making it vulnerable to this attack when developers inadvertently allow CRLF characters to reach the response headers.
The most common manifestation in Sails occurs through dynamic header manipulation. Consider this typical Sails controller pattern:
module.exports = {
setCustomHeader: async function (req, res) {
const headerName = req.query.headerName;
const headerValue = req.query.headerValue;
res.set(headerName, headerValue);
res.ok();
}
};This code allows an attacker to inject arbitrary headers by crafting requests like:
GET /setCustomHeader?headerName=Location&headerValue=%0d%0a%20Set-Cookie:%20session=malicious HTTP/1.1The URL-encoded %0d%0a represents CRLF characters. When Sails processes this, it interprets the line break and adds a Set-Cookie header, enabling session fixation attacks.
Another common pattern involves Sails's response object methods that accept dynamic header values:
module.exports = {
redirectWithHeader: async function (req, res) {
const redirectUrl = req.query.url;
const customHeader = req.query.header;
res.set('X-Custom-Header', customHeader);
res.redirect(redirectUrl);
}
};An attacker could exploit this with:
GET /redirectWithHeader?url=https://evil.com&header=%0d%0aSet-Cookie:%20session=malicious HTTP/1.1Sails's Waterline ORM can also be a vector when query results are used in headers without validation. If a database field contains CRLF characters and is directly used in a response header, injection occurs:
module.exports = {
showUserProfile: async function (req, res) {
const userId = req.params.id;
const user = await User.findOne({ id: userId });
res.set('X-User-Info', user.customHeader);
res.ok();
}
};If the customHeader database field contains malicious CRLF sequences, headers can be manipulated without any URL parameter tampering.
Sails-Specific Detection
Detecting CRLF injection in Sails requires examining both code patterns and runtime behavior. The most effective approach combines static analysis of controller code with dynamic scanning of running endpoints.
Static code analysis should look for these specific Sails patterns:
// Dangerous patterns to flag
res.set(dynamicHeaderName, dynamicValue);
res.set(dynamicValue); // when value contains header
res.location(dynamicUrl); // when URL is user-controlled
res.redirect(dynamicUrl); // when URL is user-controlledAutomated tools can scan Sails projects for these patterns, but runtime scanning provides definitive detection. middleBrick's black-box scanning approach is particularly effective for Sails applications because it tests the actual running API without requiring source code access.
When middleBrick scans a Sails endpoint, it performs these specific CRLF injection tests:
GET /vulnerable-endpoint?header=%0d%0aSet-Cookie:%20session=malicious HTTP/1.1The scanner monitors the response for unexpected headers, status code changes, or content modifications that indicate successful injection. middleBrick's 12 parallel security checks include specific CRLF detection that examines:
- Response headers for unauthorized additions
- HTTP status code manipulation
- Content-Type header changes
- Unexpected Set-Cookie headers
- Cache-control header modifications
For Sails applications using the built-in policies system, middleBrick can scan policy endpoints to verify that request preprocessing doesn't inadvertently introduce CRLF vulnerabilities:
module.exports.policies = {
'*': ['sanitizeRequest']
};The scanner tests whether the sanitizeRequest policy properly handles header manipulation attempts, providing a comprehensive security assessment of the entire Sails application stack.
Sails-Specific Remediation
Remediating CRLF injection in Sails applications requires a defense-in-depth approach using Sails's native features and Express.js security practices. The most effective strategy combines input validation, output encoding, and secure coding patterns.
The primary defense is input sanitization using Sails's built-in request object methods. Sails provides req.sanitize() through the validator middleware, which can be configured to strip dangerous characters:
module.exports = {
sanitizeHeaders: async function (req, res) {
const headerName = req.sanitize(req.query.headerName).replace(/[
]/g, '');
const headerValue = req.sanitize(req.query.headerValue).replace(/[
]/g, '');
res.set(headerName, headerValue);
res.ok();
}
};Better yet, avoid dynamic headers entirely and use predefined header names:
module.exports = {
setCustomHeader: async function (req, res) {
const headerValue = req.sanitize(req.query.headerValue).replace(/[
]/g, '');
// Only allow specific header names
const allowedHeaders = ['X-Custom-Info', 'X-User-Role'];
const headerName = allowedHeaders[0]; // or select based on safe logic
res.set(headerName, headerValue);
res.ok();
}
};For redirects, Sails provides res.redirect() which should be used with caution. Implement URL validation before redirecting:
const url = require('url');
module.exports = {
safeRedirect: async function (req, res) {
const redirectUrl = req.query.url;
// Validate URL - only allow same-origin redirects
const parsedUrl = url.parse(redirectUrl);
const currentHost = req.get('host');
if (parsedUrl.host !== currentHost) {
return res.badRequest('Invalid redirect URL');
}
// Sanitize any headers that might be set during redirect
const safeUrl = redirectUrl.replace(/[
]/g, '');
res.redirect(safeUrl);
}
};Sails's policy system provides an excellent mechanism for centralized CRLF protection. Create a policy that sanitizes all request parameters:
// api/policies/sanitizeHeaders.js
module.exports = function sanitizeHeaders (req, res, next) {
// Sanitize all query parameters
Object.keys(req.query).forEach(key => {
req.query[key] = req.query[key].replace(/[
]/g, '');
});
// Sanitize all body parameters
if (req.body) {
Object.keys(req.body).forEach(key => {
if (typeof req.body[key] === 'string') {
req.body[key] = req.body[key].replace(/[
]/g, '');
}
});
}
next();
};Apply this policy globally to protect all endpoints:
module.exports.policies = {
'*': ['sanitizeHeaders']
};For database-driven header content, implement output encoding when retrieving data:
const he = require('he'); // HTML entities encoding
module.exports = {
showUserProfile: async function (req, res) {
const userId = req.params.id;
const user = await User.findOne({ id: userId });
// Encode any user-controlled header content
const safeHeader = he.encode(user.customHeader, {
useNamedReferences: true,
allowUnsafeSymbols: false
});
res.set('X-User-Info', safeHeader);
res.ok();
}
};Finally, leverage Sails's configuration system to enforce security headers globally:
// config/security.js
module.exports.security = {
cors: {
origin: 'http://example.com',
credentials: false
},
csp: {
'default-src': ["'self'"]
},
// Other security headers automatically set by Sails
};This configuration ensures that even if CRLF injection succeeds in adding a header, the browser's security model limits the impact through Content Security Policy and other protections.