Skip to content

CWE-117: Log Injection - JavaScript/Node.js

Overview

Log Injection in JavaScript/Node.js occurs when untrusted user input is written to logs without encoding, allowing attackers to forge log entries, inject newlines to create fake events, or manipulate log analysis tools. Node.js applications using winston, bunyan, pino, or console.log are vulnerable when logging user-controlled data containing control characters like \n, \r, or ANSI escape codes.

Primary Defence: Use structured logging with JSON/ECS output (Winston/Pino JSON formatters) so control characters are encoded within fields and cannot forge log entries. Encoding (e.g., \n\\n) preserves forensic evidence of attack attempts, while removal loses visibility. Implement length limits on logged values to prevent log injection and log flooding.

Common Vulnerable Patterns

console.log with User Input

const express = require('express');
const app = express();

app.post('/login', express.json(), (req, res) => {
    const username = req.body.username;

    // VULNERABLE - User input directly in log
    console.log(`Login attempt for user: ${username}`);

    // Authenticate user...
    res.json({ success: true });
});

// Attack: POST /login with username = "admin\nSUCCESS: admin logged in"
// Log output:
// Login attempt for user: admin
// SUCCESS: admin logged in
// Result: Fake success message injected into logs

Why this is vulnerable:

  • console.log writes newline characters as real line breaks.
  • User input can inject \n/\r to create extra log lines.
  • Attackers can forge or hide events in line-based log formats.
  • Log analysis tools may treat injected lines as legitimate events.

winston Logger with String Interpolation

const winston = require('winston');

const logger = winston.createLogger({
    format: winston.format.simple(),
    transports: [new winston.transports.File({ filename: 'app.log' })]
});

app.get('/search', (req, res) => {
    const query = req.query.q;

    // VULNERABLE - String interpolation with user input
    logger.info(`Search query: ${query}`);

    // Perform search...
    res.json({ results: [] });
});

// Attack: /search?q=test%0A%0Ainfo: ADMIN ACCESS GRANTED for user: attacker
// Log output:
// info: Search query: test
// 
// info: ADMIN ACCESS GRANTED for user: attacker
// Result: Fake admin access log entry

Why this is vulnerable:

  • String interpolation does not escape newline/control characters.
  • URL-encoded newlines (%0A, %0D) become real line breaks.
  • Attackers can forge admin actions or fake success events.
  • Simple text layouts emit injected lines as separate records.

bunyan Logger with Untrusted Data

const bunyan = require('bunyan');
const log = bunyan.createLogger({ name: 'myapp' });

app.post('/update-profile', express.json(), (req, res) => {
    const email = req.body.email;

    // VULNERABLE - Email directly in log message
    log.info(`Profile updated for email: ${email}`);

    // Update profile...
    res.json({ success: true });
});

// Attack: POST with email = "user@test.com\n{\"level\":30,\"msg\":\"User promoted to admin\"}"
// Log output (JSON):
// {"level":30,"msg":"Profile updated for email: user@test.com"}
// {"level":30,"msg":"User promoted to admin"}
// Result: Fake admin promotion log

Why this is vulnerable:

  • Newlines in user input can split JSON log entries.
  • Attackers can inject new JSON objects into the log stream.
  • Forged entries can mimic legitimate fields, levels, and messages.
  • Log parsers may accept injected objects as valid events.

pino Logger with User-Controlled Fields

const pino = require('pino');
const logger = pino();

app.post('/api/action', express.json(), (req, res) => {
    const action = req.body.action;
    const userId = req.body.userId;

    // VULNERABLE - User-controlled data in log
    logger.info(`User ${userId} performed action: ${action}`);

    res.json({ success: true });
});

// Attack: userId = "123\nINFO: Security audit disabled by admin"
// Log output:
// {"level":30,"msg":"User 123"}
// INFO: Security audit disabled by admin performed action: update"}

Why this is vulnerable:

  • Template literals embed user data directly into the log string.
  • Newlines can break JSON structure and split records.
  • Structured loggers still emit line-based output.
  • Attackers can create fake entries that look legitimate.

Logging Entire Request Objects

app.post('/api/data', (req, res) => {
    // VULNERABLE - Logging entire request object
    console.log('Request received:', JSON.stringify(req.body));
    // User controls entire object structure, can inject arbitrary JSON fields

    res.json({ success: true });
});

// Attack: POST with body = {"action": "view", "level": "error", "severity": "critical", "admin": true}
// Log output: {"action":"view","level":"error","severity":"critical","admin":true}
// Result: Injected metadata confuses log analysis tools or SIEM systems

Why this is vulnerable:

  • User-controlled objects can inject arbitrary fields into logs.
  • Attackers can override metadata like level or severity.
  • SIEM/indexed fields can be polluted with misleading data.
  • JSON.stringify preserves attacker-controlled structure.

ANSI Color Code Injection

const chalk = require('chalk');

app.get('/status', (req, res) => {
    const component = req.query.component;

    // VULNERABLE - User input with ANSI codes
    console.log(chalk.blue(`Status check for: ${component}`));

    res.json({ status: 'ok' });
});

// Attack: /status?component=server%1B[31m CRITICAL ERROR %1B[0m
// Terminal output: Status check for: server CRITICAL ERROR (displayed in red)
// Result: Fake critical error message in red causes confusion

Why this is vulnerable:

  • ANSI escape sequences can change colors and formatting.
  • Attackers can fake severity indicators (e.g., red “errors”).
  • Terminal control codes can hide or erase log lines.
  • Dashboards that render ANSI codes can be misled.

Error Logging with Stack Traces

app.get('/api/data', (req, res) => {
    const filter = req.query.filter;

    try {
        // Some operation that might fail
        const data = processData(filter);
        res.json(data);
    } catch (error) {
        // VULNERABLE - User input in error context
        console.error(`Error processing filter "${filter}": ${error.message}`);
        res.status(500).json({ error: 'Processing failed' });
    }
});

// Attack: /api/data?filter=test"\nERROR: Database compromised - initiating emergency shutdown
// Log output:
// Error processing filter "test"
// ERROR: Database compromised - initiating emergency shutdown": Invalid input

Why this is vulnerable:

  • User input is embedded in error strings without encoding.
  • Newlines can inject fake critical errors into logs.
  • Forged errors can trigger false incident responses.
  • Audit trails become unreliable.

Audit Log with Timestamp Manipulation

const fs = require('fs');

function auditLog(action, user, details) {
    const timestamp = new Date().toISOString();

    // VULNERABLE - User-controlled details in audit log
    const logEntry = `[${timestamp}] ${action} by ${user}: ${details}\n`;
    fs.appendFileSync('audit.log', logEntry);
}

app.post('/admin/delete-user', express.json(), (req, res) => {
    const targetUser = req.body.targetUser;
    const reason = req.body.reason;

    auditLog('DELETE_USER', req.user.username, `Target: ${targetUser}, Reason: ${reason}`);

    // Delete user...
    res.json({ success: true });
});

// Attack: reason = "cleanup\n[2025-01-01T00:00:00.000Z] RESTORE_USER by admin: Target: admin"
// Log shows deletion followed by fake restoration entry

Why this is vulnerable:

  • File-based logs rely on line boundaries for entries.
  • Newlines can inject fake timestamps and entries.
  • Attackers can manipulate audit trails or create false alibis.
  • Simple concatenation provides no structure or encoding.

ANSI Color Code Injection

const chalk = require('chalk');

app.get('/status', (req, res) => {
    const component = req.query.component;

    // VULNERABLE - User input with ANSI codes
    console.log(chalk.blue(`Status check for: ${component}`));

    res.json({ status: 'ok' });
});

// Attack: /status?component=server%1B[31m CRITICAL ERROR %1B[0m
// Terminal output: Status check for: server CRITICAL ERROR (in red)
// Result: Fake critical error message in red, causing confusion

Why this is vulnerable:

  • ANSI escape sequences can manipulate terminal output.
  • Attackers can spoof severity or hide entries.
  • Terminal clearing codes can erase important logs.
  • Visual monitoring becomes unreliable.

Structured Logging with Object Injection

const winston = require('winston');

const logger = winston.createLogger({
    format: winston.format.json(),
    transports: [new winston.transports.File({ filename: 'structured.log' })]
});

app.post('/api/event', express.json(), (req, res) => {
    const eventType = req.body.eventType;
    const metadata = req.body.metadata;

    // VULNERABLE - Logging entire user-controlled object
    logger.info('Event received', {
        type: eventType,
        data: metadata  // User can inject arbitrary fields
    });

    res.json({ success: true });
});

// Attack: metadata = { "level": "error", "severity": "critical", "admin": true }
// Log output: May confuse log analysis tools or SIEM systems

Why this is vulnerable:

  • User-controlled objects can inject arbitrary fields.
  • Metadata fields like level and severity can be spoofed.
  • Indexed fields in SIEMs can be polluted.
  • Analysts may trust forged structured fields.

Key Security Functions

General Log Encoding

This encode full control range to preserve forensic evidence.

function encodeForSingleLineTextLog(input) {
    if (input === null || input === undefined) {
        return '[null]';
    }

    if (typeof input !== 'string') {
        input = String(input);
    }

    const encodeControl = (ch) => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0');
    return input
        .replace(/\\/g, '\\\\')     // Encode backslashes
        .replace(/\r/g, '\\r')      // Encode carriage returns
        .replace(/\n/g, '\\n')      // Encode newlines (shows attack attempts)
        .replace(/\t/g, '\\t')      // Encode tabs
        .replace(/[\x00-\x1F\x7F-\x9F]/g, encodeControl)  // Encode ASCII/C1 controls
        .replace(/\u2028/g, '\\u2028')  // Unicode line separator
        .replace(/\u2029/g, '\\u2029')  // Unicode paragraph separator
        .substring(0, 1000);        // Limit length to prevent DoS
    // ✓ Preserves evidence: "test\\nFAKE LOG" instead of "testFAKE LOG"
}

Validate Email for Logging

function validateEmailForLog(email) {
    const emailRegex = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

    if (!emailRegex.test(email)) {
        return '[invalid-email]';
    }

    return email.substring(0, 100);
}

Winston Encoding Format

const winston = require('winston');

const encodeFormat = winston.format((info) => {
    // Encode the main message
    if (info.message) {
        info.message = encodeForSingleLineTextLog(info.message);
    }

    // Encode metadata fields
    Object.keys(info).forEach(key => {
        if (typeof info[key] === 'string' && !['level', 'timestamp'].includes(key)) {
            info[key] = encodeForSingleLineTextLog(info[key]);
        }
    });

    return info;
});

const logger = winston.createLogger({
    format: winston.format.combine(
        encodeFormat(),
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [new winston.transports.File({ filename: 'app.log' })]
});

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

Framework-Specific Log Injection Patterns

Express.js with winston

const express = require('express');
const winston = require('winston');

const logger = winston.createLogger({
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' })
    ]
});

// Middleware: Add request ID
app.use((req, res, next) => {
    req.id = require('crypto').randomBytes(16).toString('hex');
    next();
});

// Secure logging middleware
app.use((req, res, next) => {
    logger.info('Request received', {
        requestId: req.id,
        method: req.method,
        path: req.path,
        ip: req.ip
    });
    next();
});

app.post('/api/action', express.json(), (req, res) => {
    const action = encodeForSingleLineTextLog(req.body.action);

    logger.info('Action performed', {
        requestId: req.id,
        userId: req.user?.id,
        action: action
    });

    res.json({ success: true });
});

NestJS with pino

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import pino from 'pino';

const logger = pino({
    level: process.env.LOG_LEVEL || 'info',
    formatters: {
        level: (label) => {
            return { level: label };
        }
    }
});

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction) {
        const encode = (input: any): string => {
            return String(input)
                .replace(/[\n\r]/g, ' ')
                .substring(0, 200);
        };

        logger.info({
            method: req.method,
            path: req.path,
            userId: (req as any).user?.id,
            ip: req.ip
        }, 'Request received');

        next();
    }
}

Secure Patterns

function encodeForSingleLineTextLog(input) {
    if (input === null || input === undefined) {
        return '[null]';
    }
    const text = String(input);
    const encodeControl = (ch) => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0');
    // RECOMMENDED: Encode full control range to preserve forensic evidence
    return text
        .replace(/\\/g, '\\\\')  // Encode backslashes
        .replace(/\r/g, '\\r')  // Encode carriage returns
        .replace(/\n/g, '\\n')  // Encode (not remove) - shows "test\\nFAKE LOG"
        .replace(/\t/g, '\\t')  // Encode tabs (visible)
        .replace(/[\x00-\x1F\x7F-\x9F]/g, encodeControl)  // Encode ASCII/C1 controls
        .replace(/\u2028/g, '\\u2028')  // Unicode line separator
        .replace(/\u2029/g, '\\u2029')  // Unicode paragraph separator
        .substring(0, 500);
    // ✓ Security team sees the full attack attempt in logs
}

app.post('/login', express.json(), (req, res) => {
    const username = encodeForSingleLineTextLog(req.body.username);
    console.log(`Login attempt for user: ${username}`);
    // Attack "admin\nFAKE" logs as: "admin\\nFAKE" (attack visible but safe)
    res.json({ success: true });
});

Why this works:

  • Encodes the full control range to preserve forensic evidence of attack attempts.
  • Converts inputs to strings to avoid logging errors.
  • Length caps reduce log-based DoS and disk bloat.
  • Sanitization at the boundary protects all loggers consistently.
  • Superior to removal: Shows "test\nFAKE" instead of "testFAKE", making attacks visible.
  • Encoding control chars blocks ANSI escape injection and log forging.

Structured logging with winston (JSON/ECS)

const winston = require('winston');

const logger = winston.createLogger({
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [new winston.transports.File({ filename: 'app.log' })]
});

app.get('/search', (req, res) => {
    logger.info('Search performed', {
        query: encodeForSingleLineTextLog(req.query.q),
        userId: req.user?.id,
        requestId: req.id
    });
    res.json({ results: [] });
});

Why this works:

  • JSON/ECS output isolates data from log structure.
  • Control characters are encoded within JSON fields, not emitted as new records.
  • Sanitization removes dangerous bytes before serialization (optional but recommended).
  • Structured fields avoid string interpolation attacks.
  • Request-scoped metadata improves traceability safely.

Pino with length and character limits (JSON)

const pino = require('pino');
const logger = pino();

function safeValue(value) {
    if (typeof value !== 'string') return value;
    return value.replace(/[\n\r\t]/g, ' ').substring(0, 300);
}

app.post('/api/action', express.json(), (req, res) => {
    logger.info({
        action: safeValue(req.body.action),
        userId: safeValue(req.body.userId),
        ts: Date.now()
    }, 'User action');
    res.json({ success: true });
});

Why this works:

  • Structured JSON keeps attacker input as data, not formatting.
  • Control characters are stripped to prevent line breaks.
  • Length limits reduce disk bloat and SIEM overload.
  • Avoiding template literals prevents log structure corruption.

Bunyan with validation and allowlists

const bunyan = require('bunyan');
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const log = bunyan.createLogger({
    name: 'myapp',
    serializers: bunyan.stdSerializers
});

app.post('/update-profile', express.json(), (req, res) => {
    const email = emailRegex.test(req.body.email) ? req.body.email : '[invalid-email]';
    log.info({
        event: 'profile_update',
        email,
        userId: req.user?.id
    });
    res.json({ success: true });
});

Why this works:

  • Validation/normalization prevents arbitrary field injection.
  • Structured logging keeps fields isolated.
  • Invalid inputs are replaced with safe placeholders.
  • Prevents spoofed admin events or metadata overrides.

Audit log with JSON append

const fs = require('fs');

function secureAuditLog(action, user, details) {
    const entry = {
        timestamp: new Date().toISOString(),
        action,
        user,
        details: encodeForSingleLineTextLog(details)
    };
    fs.appendFileSync('audit.log', JSON.stringify(entry) + '\n');
}

app.post('/admin/delete-user', express.json(), (req, res) => {
    secureAuditLog('DELETE_USER', req.user.username, req.body.reason);
    res.json({ success: true });
});

Why this works:

  • JSON lines enforce one entry per line.
  • Sanitization strips control characters from details.
  • Attackers cannot forge timestamps or extra entries.
  • Log analysis tools can detect tampering reliably.

Disable ANSI codes in production

const isProduction = process.env.NODE_ENV === 'production';

const logger = winston.createLogger({
    format: winston.format.combine(
        winston.format.timestamp(),
        isProduction ? winston.format.json() : winston.format.simple()
    ),
    transports: [new winston.transports.File({ filename: 'app.log' })]
});

app.get('/status', (req, res) => {
    logger.info('Status check', { component: encodeForSingleLineTextLog(req.query.component) });
    res.json({ status: 'ok' });
});

Why this works:

  • Disables ANSI escape vectors in production logs.
  • Sanitization neutralizes remaining control characters.
  • JSON output keeps logs structured and unambiguous.
  • Colorized output is limited to safe non-production use.

Typical Log Injection Findings

  1. "Untrusted data written to log without encoding"

    • Location: console.log(\User: ${username}`)`
    • Fix: Encode username: encodeForSingleLineTextLog(username)
  2. "User input in log message may contain newlines"

    • Location: logger.info('Query: ' + query)
    • Fix: Use structured logging: logger.info('Query performed', { query })
  3. "Control characters in log output"

    • Location: Direct logging of user input
    • Fix: Use JSON/ECS output or encode control characters before logging
  4. "Log injection via HTTP headers"

    • Location: console.log(req.headers['user-agent'])
    • Fix: Encode header values or use structured logging
  5. "ANSI escape sequences in logs"

    • Location: Colorized logging with user input
    • Fix: Disable colors in production, encode input

Security Checklist

  • Encode all user input before logging (do not remove \n, \r, control characters)
  • Use structured logging (JSON format) to separate data from log structure
  • Limit length of logged values (prevent DoS)
  • Validate input format before logging (email, phone, etc.)
  • Use winston/pino/bunyan with JSON format for production
  • Never log sensitive data (passwords, API keys, tokens)
  • Disable ANSI color codes in production logs
  • Implement log rotation and size limits
  • Use allowlists for log metadata fields
  • Test with payloads containing \n, \r, ANSI codes
  • Monitor logs for suspicious patterns or injection attempts
  • Review log aggregation tool configurations for security

Additional Resources