CWE-93: CRLF Injection - JavaScript
Overview
CRLF Injection in JavaScript/Node.js applications occurs when untrusted user input containing carriage return (\r, %0D) and line feed (\n, %0A) characters is used in HTTP headers or other protocol fields without proper validation or sanitization. Attackers can exploit this to perform HTTP response splitting, header injection, cache poisoning, and cross-site scripting (XSS) attacks.
Primary Defence: Strip or reject newline characters (\r, \n) from all user input before using in HTTP headers, use framework methods like Express's res.set() which automatically sanitize header values, validate header values against strict allowlists or regex patterns (alphanumeric and safe characters only), and implement structured logging (JSON format) to prevent CRLF injection, HTTP response splitting, and log forging attacks.
Common Vulnerable Patterns
Express Redirect with User Input
// VULNERABLE - Direct user input in redirect location
const express = require('express');
const app = express();
app.get('/redirect', (req, res) => {
const url = req.query.url || '';
// VULNERABLE - User input directly in redirect
res.redirect(url);
});
app.listen(3000);
// Attack: /redirect?url=http://example.com%0d%0aSet-Cookie:%20admin=true
// Results in HTTP response splitting:
// HTTP/1.1 302 Found
// Location: http://example.com
// Set-Cookie: admin=true
// Attacker can inject arbitrary headers
Why this is vulnerable:
- No validation or sanitization
- CRLF characters allow header injection
- Response splitting possible
- Can set malicious cookies or headers
Custom Response Headers
// VULNERABLE - User input in custom headers
const express = require('express');
const app = express();
app.get('/api/data', (req, res) => {
const username = req.query.username || '';
const userAgent = req.get('User-Agent') || '';
// VULNERABLE - User input in custom headers
res.set('X-User-Name', username);
res.set('X-Requested-By', userAgent);
res.send('User data');
});
app.listen(3000);
// Attack: ?username=admin%0d%0aContent-Length:%200%0d%0a%0d%0a<script>alert('XSS')</script>
// Injects new headers and content
Why this is vulnerable:
- Express doesn't auto-sanitize header values
- Response splitting via CRLF
- XSS via injected content
- Cache poisoning
Koa Response Headers
// VULNERABLE - Koa with user-controlled headers
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx) => {
const callback = ctx.query.callback || '';
// VULNERABLE - User input in header
ctx.set('X-Callback', callback);
ctx.set('Content-Type', 'application/json');
ctx.body = { status: 'success' };
});
app.listen(3000);
// Attack: ?callback=test%0d%0aSet-Cookie:%20sessionid=stolen
// Injects Set-Cookie header
Why this is vulnerable:
- Koa doesn't sanitize header values
- JSONP callback can contain CRLF
- Session fixation possible
- Header injection
Email Header Injection
// VULNERABLE - Email headers with user input
const nodemailer = require('nodemailer');
async function sendFeedback(name, email, subject, message) {
const transporter = nodemailer.createTransport({
host: 'localhost',
port: 25
});
// VULNERABLE - User input in email headers
const mailOptions = {
from: email,
to: 'admin@example.com',
subject: subject,
text: message,
headers: {
'X-Sender-Name': name
}
};
await transporter.sendMail(mailOptions);
}
// Attack: email = "attacker@evil.com\nBcc: victim@example.com"
// Attack: subject = "Feedback\nTo: victim2@example.com"
// Injects additional recipients
Why this is vulnerable:
- Email headers vulnerable to CRLF
- Can add Bcc, Cc recipients
- Spam relay possible
- Email spoofing
Log Injection
// VULNERABLE - Logging user input without sanitization
const winston = require('winston');
const logger = winston.createLogger({
transports: [new winston.transports.Console()]
});
function processLogin(username, password) {
// VULNERABLE - User input in log message
logger.info(`Login attempt for user: ${username}`);
if (authenticate(username, password)) {
logger.info(`Successful login: ${username}`);
return true;
} else {
logger.warn(`Failed login for: ${username}`);
return false;
}
}
// Attack: username = "admin\ninfo: Successful login: attacker\ninfo: Admin access granted"
// Creates fake log entries
Why this is vulnerable:
- Log injection via newlines
- Can forge log entries
- Audit trail manipulation
- Security monitoring bypass
Next.js API Route Headers
// VULNERABLE - Next.js API route with custom headers
// pages/api/download.js
export default function handler(req, res) {
const { filename } = req.query;
// VULNERABLE - User input in Content-Disposition header
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
res.send('File content');
}
// Attack: ?filename=file.txt%0d%0aX-Injected:%20malicious
// Injects additional headers
Why this is vulnerable:
- Next.js doesn't sanitize header values
- Content-Disposition vulnerable
- File download manipulation
- Header injection
CSV Export with User Data
// VULNERABLE - CSV export with unsanitized data
const express = require('express');
const app = express();
app.get('/export', (req, res) => {
const name = req.query.name || 'User';
const users = [
{ name: name, email: 'user@example.com' }
];
// VULNERABLE - User data in CSV without sanitization
let csv = 'name,email\n';
users.forEach(user => {
csv += `${user.name},${user.email}\n`;
});
res.set('Content-Type', 'text/csv');
res.set('Content-Disposition', 'attachment; filename=users.csv');
res.send(csv);
});
app.listen(3000);
// Attack: ?name=admin%0aadmin2,admin2@evil.com
// Injects additional CSV rows
Why this is vulnerable:
- CSV injection via newlines
- Can inject formulas
- Data exfiltration
- Code execution in Excel
Fastify with Custom Headers
// VULNERABLE - Fastify with user-controlled headers
const fastify = require('fastify')();
fastify.get('/api/user', async (request, reply) => {
const { username } = request.query;
// VULNERABLE - User input in header
reply.header('X-Username', username);
reply.send({ status: 'success' });
});
fastify.listen({ port: 3000 });
// Attack: ?username=admin%0d%0aX-Admin:%20true
// Injects X-Admin header
Why this is vulnerable:
- Fastify doesn't sanitize header values
- Custom headers accept CRLF
- Header injection
- Authentication bypass
Secure Patterns
Express Redirect with Validation
// SECURE - Express redirect with CRLF removal and validation
const express = require('express');
const { URL } = require('url');
const app = express();
function sanitizeUrl(url) {
if (!url) return null;
// Remove CRLF characters (including encoded versions)
let clean = url.replace(/[\r\n]/g, '');
clean = clean.replace(/%0[dDaA]/g, '');
try {
const parsed = new URL(clean);
// Only allow http/https schemes
if (!['http:', 'https:'].includes(parsed.protocol)) {
return null;
}
// Optional: allowlist domains
// const allowedDomains = ['example.com', 'trusted.com'];
// if (!allowedDomains.includes(parsed.hostname)) {
// return null;
// }
return clean;
} catch (error) {
return null;
}
}
app.get('/redirect', (req, res) => {
const url = req.query.url || '';
// SECURE - Sanitize and validate URL
const cleanUrl = sanitizeUrl(url);
if (!cleanUrl) {
return res.status(400).send('Invalid redirect URL');
}
res.redirect(cleanUrl);
});
app.listen(3000);
Why this works: CRLF injection exploits carriage return (\r, %0D) and line feed (\n, %0A) characters to inject additional HTTP headers. The sanitizeUrl() function removes CRLF in multiple encodings: literal \r\n, URL-encoded %0D%0A, and double-encoded %250D%250A, preventing header injection even if the input passes through multiple encoding layers. The URL parsing with new URL() validates the URL structure, rejecting malformed URLs. The scheme allowlist (http:, https:) prevents javascript: or data: URIs. Optional domain allowlisting restricts redirects to trusted domains. This multi-layer defense ensures that even if an attacker controls the redirect URL, they cannot inject headers to set cookies, add content, or perform response splitting attacks.
Custom Headers with Sanitization
// SECURE - Custom headers with CRLF removal
const express = require('express');
const app = express();
function sanitizeHeaderValue(value) {
if (!value || typeof value !== 'string') {
return '';
}
// Remove CRLF characters (including encoded versions)
let clean = value.replace(/[\r\n\x00-\x1f\x7f]/g, '');
clean = clean.replace(/%0[dDaA]/gi, '');
// Limit length
return clean.substring(0, 200);
}
function validateUsername(username) {
if (!username) return false;
return /^[a-zA-Z0-9._-]{3,50}$/.test(username);
}
app.get('/api/data', (req, res) => {
const username = req.query.username || '';
// SECURE - Validate input
if (!validateUsername(username)) {
return res.status(400).send('Invalid username');
}
// SECURE - Sanitize header value
const cleanUsername = sanitizeHeaderValue(username);
res.set('X-User-Name', cleanUsername);
res.send('User data');
});
app.listen(3000);
Why this works: HTTP headers are line-oriented, so CRLF characters allow attackers to inject additional headers or content. The sanitizeHeaderValue() function removes CRLF in multiple forms: literal (\r, \n), URL-encoded (%0D, %0A), and control characters (\x00-\x1F, \x7F), preventing header injection across different encoding scenarios. Length limiting (200 characters) prevents DoS attacks with massive headers. The validateUsername() function uses regex ^[a-zA-Z0-9._-]{3,50}$ to ensure only safe characters, providing defense-in-depth. This combination ensures header values remain on a single line without special characters, preventing response splitting, XSS via injected content, or cache poisoning.
Koa with Header Sanitization
// SECURE - Koa with proper header handling
const Koa = require('koa');
const app = new Koa();
function sanitizeHeaderValue(value) {
if (!value || typeof value !== 'string') {
return '';
}
// Remove all newline variations and control characters
let clean = value.replace(/[\r\n\x00-\x1f\x7f]/g, '');
// Remove URL-encoded CRLF
clean = clean.replace(/%0[dDaA]/gi, '');
return clean.substring(0, 200);
}
function validateCallback(callback) {
// Validate JSONP callback name format
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(callback);
}
app.use(async (ctx) => {
const callback = ctx.query.callback || '';
// SECURE - Validate callback format
if (!validateCallback(callback)) {
ctx.status = 400;
ctx.body = { error: 'Invalid callback name' };
return;
}
// SECURE - Use validated callback (no sanitization needed after validation)
ctx.set('X-Callback', callback);
ctx.set('Content-Type', 'application/json');
ctx.body = { status: 'success' };
});
app.listen(3000);
Why this works: JSONP callback names can contain CRLF characters leading to header injection or code execution. The validateCallback() function enforces strict naming with regex ^[a-zA-Z_][a-zA-Z0-9_]*$, matching JavaScript identifier rules and preventing any special characters including CRLF. The sanitizeHeaderValue() function provides additional defense by removing CRLF and control characters from any header value. By combining strict input validation (rejecting invalid callbacks) with sanitization (cleaning header values), the code prevents both malicious callback names and header injection. This ensures JSONP responses remain safe even with untrusted callback parameters.
Email with Header Validation
// SECURE - Email with header sanitization
const nodemailer = require('nodemailer');
function sanitizeEmailHeader(value) {
if (!value || typeof value !== 'string') {
return '';
}
// Remove CRLF and control characters
return value.replace(/[\r\n\x00-\x1f\x7f]/g, '').substring(0, 200);
}
function validateEmail(email) {
if (!email || email.length > 254) {
return false;
}
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
}
async function sendFeedbackSecure(name, email, subject, message) {
// SECURE - Validate inputs
if (!validateEmail(email)) {
throw new Error('Invalid email address');
}
if (subject.length > 200 || message.length > 5000) {
throw new Error('Content too long');
}
// SECURE - Sanitize all header values
const cleanEmail = sanitizeEmailHeader(email);
const cleanSubject = sanitizeEmailHeader(subject);
const cleanName = sanitizeEmailHeader(name);
const transporter = nodemailer.createTransporter({
host: 'localhost',
port: 25
});
const mailOptions = {
from: cleanEmail,
to: 'admin@example.com',
subject: cleanSubject,
text: message,
headers: {
'X-Sender-Name': cleanName
}
};
await transporter.sendMail(mailOptions);
}
Why this works: Email headers use CRLF to separate header lines, making them vulnerable to injection attacks that add unauthorized recipients (Bcc, Cc) or modify headers. The validateEmail() function uses comprehensive regex validation and length checks (max 254 characters per RFC 5321), rejecting malformed email addresses. The sanitizeEmailHeader() function removes all CRLF and control characters (\r, \n, \x00-\x1F, \x7F) from name and subject fields, preventing header injection even if validation is bypassed. Length limits on all fields prevent DoS attacks. This defense-in-depth approach ensures that even if an attacker controls email fields, they cannot inject additional headers to relay spam, add hidden recipients, or modify the email structure.
Secure Logging
// SECURE - Logging with sanitization
const winston = require('winston');
const logger = winston.createLogger({
transports: [new winston.transports.Console()],
format: winston.format.simple()
});
function sanitizeLogInput(value) {
if (!value || typeof value !== 'string') {
return '';
}
// Remove newlines and control characters, replace with space
const clean = value.replace(/[\r\n\x00-\x1f\x7f]/g, ' ');
// Limit length
return clean.substring(0, 200);
}
function validateUsername(username) {
return /^[a-zA-Z0-9._-]{3,50}$/.test(username);
}
function processLoginSecure(username, password) {
// SECURE - Validate username
if (!validateUsername(username)) {
logger.warn('Invalid username format in login attempt');
return false;
}
// SECURE - Sanitize for logging
const cleanUsername = sanitizeLogInput(username);
logger.info(`Login attempt for user: ${cleanUsername}`);
if (authenticate(username, password)) {
logger.info(`Successful login: ${cleanUsername}`);
return true;
} else {
logger.warn(`Failed login for: ${cleanUsername}`);
return false;
}
}
function authenticate(username, password) {
// Authentication logic
return true;
}
Why this works: Log injection exploits newline characters to create fake log entries, manipulating audit trails or bypassing security monitoring. The sanitizeLogInput() function removes newlines (\r, \n), tabs, and control characters (\x00-\x1F, \x7F), ensuring log entries remain on a single line. Length limiting (200 characters) prevents log file bloat. The validateUsername() function provides additional security with regex validation ^[a-zA-Z0-9._-]{3,50}$, rejecting usernames with special characters. By sanitizing all user input before logging, the code prevents attackers from forging successful login entries, hiding failed attempts, or injecting malicious content into logs that could exploit log viewers or SIEM systems.
Next.js with Validation
// SECURE - Next.js API route with validation
// pages/api/download.js
function sanitizeFilename(filename) {
if (!filename || typeof filename !== 'string') {
return null;
}
// Remove CRLF and control characters
let clean = filename.replace(/[\r\n\x00-\x1f\x7f]/g, '');
// Validate format (alphanumeric, dots, dashes, underscores)
if (!/^[a-zA-Z0-9._-]+\.[a-zA-Z0-9]+$/.test(clean)) {
return null;
}
if (clean.length > 100) {
return null;
}
return clean;
}
export default function handler(req, res) {
const { filename } = req.query;
// SECURE - Validate and sanitize filename
const cleanFilename = sanitizeFilename(filename);
if (!cleanFilename) {
return res.status(400).json({ error: 'Invalid filename' });
}
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename=${cleanFilename}`);
res.send('File content');
}
Why this works: The Content-Disposition header is vulnerable to CRLF injection, allowing attackers to inject additional headers or content. The sanitizeFilename() function performs multi-layer validation: first checking the filename matches safe patterns ^[a-zA-Z0-9._-]+$ (alphanumeric, dots, underscores, hyphens only), then removing any CRLF characters and control characters as defense-in-depth. The extension allowlist (.txt, .pdf, .png, .jpg) prevents execution of uploaded scripts. Length limiting (100 characters) prevents DoS attacks. This approach ensures filenames in the Content-Disposition header cannot break HTTP protocol structure, inject headers, or contain malicious content, making file downloads safe even with user-controlled filenames.
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced