Skip to content

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

Additional Resources