Crlf Injection in Sails with Basic Auth
Crlf Injection in Sails with Basic Auth — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when an attacker can inject carriage return (CR, \r) and line feed (\n) sequences into a header or status-line context. In Sails.js, this typically arises when user-controlled data is reflected in HTTP response headers without sanitization. When Basic Auth is involved, the Authorization header is parsed by the framework and the credentials are available in req.headers.authorization. If a Sails controller or policy uses that value to construct a Location header, a custom header, or influences a redirect without validation, an attacker can embed %0d%0a (or raw CR/LF) to split headers and inject new ones.
Consider a password-reset flow: after a successful Basic Auth login, the controller builds a redirect using a user-supplied next query parameter. If the value is not validated, an attacker can provide https://example.com/%0d%0aLocation:%20https://evil.com. The resulting response might include two Status lines or two Location headers, causing clients to follow the attacker’s URL. Even when Sails does not directly expose header manipulation, underlying adapters or custom hooks may forward headers to downstream services; injected CR/LF can break protocol parsing there, enabling response splitting or cache poisoning.
With Basic Auth, the credentials themselves are base64-encoded but not inherently risky; the risk comes from how the application uses request metadata. For example, a developer might log the Authorization header or use it to personalize messages. If any part of that processing reflects user input into headers or status codes, Crlf Injection becomes feasible. The unauthenticated attack surface scanned by middleBrick includes endpoints that accept user input in headers and redirects, making this a relevant vector in black-box testing. Findings related to header injection map to OWASP API Top 10:2023 —2025 (Broken Object Level Authorization and Security Misconfiguration) and can align with PCI-DSS and SOC2 controls around output handling.
An illustrative vulnerable Sails controller is shown below. It reads the Authorization header and uses a query parameter to redirect, which can be exploited via CRLF Injection if the input is not sanitized.
// api/controllers/AuthController.js
module.exports = {
login: function (req, res) {
const next = req.query.next || '/dashboard';
const auth = req.headers.authorization; // Basic base64
// Vulnerable: using user input directly in a redirect header
return res.redirect(next);
}
};Basic Auth-Specific Remediation in Sails — concrete code fixes
Remediation centers on strict validation and encoding of any user-influenced data that reaches headers or status lines. For redirects, prefer an allowlist of safe paths and avoid reflecting raw query parameters. When working with Basic Auth, treat the Authorization header as untrusted and do not reflect it into headers or responses.
1) Validate and normalize redirect targets: Use a whitelist or a strict regex to ensure the target is a safe relative path or a permitted host. Reject any input containing CR, LF, or URL-encoded equivalents (%0d, %0a, %0d%0a).
// api/controllers/AuthController.js
const ALLOWED_HOSTS = new Set(['https://app.example.com', 'https://dashboard.example.com']);
function isValidRedirect(url) {
try {
const u = new URL(url, 'https://placeholder.example.com');
return ALLOWED_HOSTS.has(u.origin) && !u.pathname.includes('\r') && !u.pathname.includes('\n');
} catch (e) {
return false;
}
}
module.exports = {
login: function (req, res) {
const next = req.query.next;
const target = isValidRedirect(next) ? next : '/dashboard';
return res.redirect(target);
}
};
2) Sanitize headers and avoid reflection: If you must include user data in custom headers, percent-encode or strip CR/LF. Do not pass raw Authorization header values into header-setting functions.
// api/actions/safe-set-header.js
module.exports = {
friendlyName: 'Safe set header',
description: 'Sets a header after stripping control characters.',
inputs: {
headerName: { type: 'string', required: true },
headerValue: { type: 'string', required: true }
},
fn: function (inputs, exits) {
const sanitizedValue = inputs.headerValue.replace(/[\r\n]+/g, '');
res.set(inputs.headerName, sanitizedValue);
return exits.success();
}
};
3) Use Sails policies to enforce authentication safely: Policies can inspect req.headers.authorization but should not forward it to views or headers. Instead, resolve the user and store a safe identifier in req.user.
// api/policies/authenticate.js
module.exports = async function (req, res, proceed) {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Basic ')) {
return res.unauthorized('Missing basic auth');
}
try {
const token = auth.split(' ')[1];
const decoded = Buffer.from(token, 'base64').toString('utf8');
const [username, password] = decoded.split(':');
// Validate credentials against your user service
const user = await User.findOne({ username, password });
if (!user) throw new Error('Invalid credentials');
req.user = { id: user.id, username: user.username };
return proceed();
} catch (err) {
return res.unauthorized('Invalid authorization header');
}
};
These measures reduce the attack surface and align with secure coding practices. They also complement scans performed by tools like middleBrick, which checks for header injection and other misconfigurations across the unauthenticated attack surface. With the Pro plan, you can enable continuous monitoring and CI/CD integration via the GitHub Action to fail builds if a risk score drops below your chosen threshold, helping to catch regressions early.