Safeguard
Application Security

How to prevent log injection vulnerabilities in Node.js

Log injection lets attackers forge log entries in Node.js apps via unsanitized input. Learn the sanitization, encoding, and structured-logging fixes that stop it.

Priya Mehta
DevSecOps Engineer
6 min read

Log injection happens when untrusted input reaches a logging call unsanitized, letting an attacker forge log entries, inject fake timestamps, split log lines, or smuggle ANSI escape sequences and terminal control characters into files that downstream tools (SIEMs, log viewers, CI consoles) will render or parse. In Node.js, the most common root cause is string concatenation or template literals passed directly into console.log, winston, pino, or bunyan calls — for example console.log('Login attempt: ' + req.body.username). If a user submits a username containing \n and a forged "admin login succeeded" line, that fabricated entry lands in your logs indistinguishable from a real one. CVE-2021-3765 in js-cleanup and multiple advisories against logging middleware in Express apps trace back to exactly this pattern. This post covers how log injection works in Node.js, which packages are commonly affected, and the concrete sanitization, encoding, and structured-logging practices that stop it.

What is log injection and how does it work in Node.js?

Log injection is the insertion of attacker-controlled characters — typically newlines (\n, \r), ANSI escape codes (\x1b), or delimiter characters — into application input that later gets written into log files or streams without encoding. In Node.js, this almost always happens at the point where a request value (header, query param, form field, JWT claim) is interpolated into a log message. Consider:

app.post('/login', (req, res) => {
  console.log(`Login attempt for user: ${req.body.username}`);
});

If req.body.username is "attacker\n2026-07-06T10:00:00Z INFO Login attempt for user: admin — SUCCESS", the resulting log stream contains two lines, the second of which looks like a legitimate successful admin login. Security teams reviewing logs manually, or SIEM correlation rules matching on INFO.*SUCCESS, can be fooled outright. Beyond newline injection, attackers can inject ANSI escape sequences that clear terminal history or reposition cursors when logs are tail -f'd in a terminal — a technique documented in OWASP's Log Injection page since 2007 and still unpatched in a large share of Express and Fastify starter templates as of 2026.

Which npm packages have shipped log injection vulnerabilities?

Yes — several widely used logging and utility packages have had documented log injection or related advisories, and the pattern recurs because template-literal logging is idiomatic in Node.js. js-cleanup (CVE-2021-3765) allowed injection through unsanitized string handling that propagated into build and log output. More broadly, GitHub's CodeQL and Semgrep both ship dedicated rules — js/log-injection and javascript.express.log-injection respectively — because the finding is common enough across winston, morgan, and hand-rolled console.log wrappers to warrant a standing rule rather than a one-off advisory. Snyk's 2025 State of Open Source Security report listed injection-class findings (which include log injection) as present in roughly 1 in 8 scanned Node.js repositories with any user-input-to-sink data flow. The common thread in every case: raw request data reaches a sink function (console.log, a logger's .info()/.warn(), or a fs.appendFile call) with no encoding step in between.

How do you sanitize user input before logging in Node.js?

You sanitize by stripping or encoding CR/LF and control characters from any value before it is interpolated into a log message, not by trusting upstream validation alone. A minimal sanitizer:

function sanitizeForLog(value) {
  return String(value)
    .replace(/[\r\n]/g, '')      // strip newlines to block line injection
    .replace(/[\x00-\x1F\x7F]/g, ''); // strip control/ANSI escape chars
}

console.log(`Login attempt for user: ${sanitizeForLog(req.body.username)}`);

This single function closes the newline-injection and ANSI-escape-injection vectors for any string passed through it. For encoding instead of stripping — useful when you want to preserve the original value for forensics — replace \n with the literal string \\n and non-printable characters with their \xHH hex representation rather than deleting them. OWASP's ESAPI and its Node equivalents follow this encode-don't-strip pattern specifically so audit trails aren't silently truncated. Apply the sanitizer at the logging call site, not just at the input-validation layer at your API boundary — data can reach a log sink from internal service calls, queue consumers, or cron jobs that never pass through your HTTP validation middleware at all.

Does structured logging with JSON prevent log injection?

Mostly yes — structured JSON logging via pino or winston's JSON formatter eliminates the newline-injection vector because values are serialized as JSON string fields rather than concatenated into free text, so an embedded \n becomes the two-character sequence \ n inside a quoted string, not an actual line break. Compare:

// Vulnerable: string concatenation
console.log('User: ' + username);

// Safe: structured logging with pino
const logger = require('pino')();
logger.info({ event: 'login_attempt', username }, 'Login attempt received');

With pino, the output is always a single-line JSON object: {"level":30,"event":"login_attempt","username":"attacker\\n2026-...","msg":"Login attempt received"}. The embedded newline is escaped by JSON.stringify internals and cannot break the log into a second line. This is why Safeguard's own scanning rules treat unstructured console.log calls carrying request data as higher-severity findings than the equivalent pino/winston JSON call — the blast radius of an unescaped newline is categorically different. Structured logging does not, however, protect a downstream consumer that later renders the username field into a terminal or HTML page without its own encoding — that's a second, separate injection point you still have to handle.

What should a Node.js logging policy require to stay safe?

A safe policy requires four things: mandatory sanitization or JSON-structured output for every log call that includes user-controlled data, a lint rule that flags raw template-literal logging, a documented list of fields treated as untrusted (headers, body, query, JWT claims, webhook payloads), and periodic review of log-consuming tooling for injection handling. Concretely, add eslint-plugin-no-unsanitized or a custom Semgrep rule to CI that fails a build when console.log/logger.info is called with an unescaped template literal containing a request-derived variable — this catches the CVE-2021-3765-style pattern before merge rather than in production. Pair that with a pino or winston base logger configured with redact paths for known-sensitive fields (auth tokens, session cookies) so sanitization and redaction happen in one place instead of being reimplemented ad hoc at every call site across a 50,000-line codebase.

How Safeguard Helps

Safeguard's reachability analysis identifies which log-injection-prone code paths actually receive attacker-controlled input in your Node.js services, so your team fixes the handful of routes where req.body, req.headers, or webhook payloads flow into an unsanitized console.log or logger call — instead of triaging every match a pattern-based scanner flags across the repo. Griffin AI reviews the surrounding data flow to confirm whether a given sink is truly exploitable versus already guarded by a sanitizer or structured-logging wrapper, cutting false positives on this specific finding class dramatically compared to regex-only tools. Safeguard's SBOM generation and ingest track which logging libraries (winston, pino, bunyan, morgan) and versions are in use across every repo, flagging any with open log-injection advisories the moment they're published. When a fix is available, Safeguard opens an auto-fix PR that wraps the vulnerable call site with a sanitization helper or migrates it to structured JSON logging, so the remediation lands as a reviewable diff rather than a ticket in a backlog.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.