Log injection

Log injection through untrusted input

Description

Log injection occurs when untrusted input is written directly to logs. An attacker may insert newlines (\n, \r), tabs, ANSI escapes, or HTML to split records or make text appear to be a separate event. Forged messages may hide real events or mislead operators. Logs rendered as HTML may also expose a web viewer to XSS.

Potential impact

  • Newlines and control characters may forge records or change a log entry's apparent meaning.
  • Broken records may interfere with line-based SIEM parsing and alerting.
  • Forged logs may delay investigation and incident response.
  • HTML-based log viewers may execute injected scripts.
  • Corrupted audit trails may undermine compliance and forensic analysis.

Remediation

  • Remove or escape \n, \r, and other relevant control characters for the log format, for example with value.replace(/[\r\n]/g, '').
  • Keep the format string fixed, as in console.log('%s', value). %s does not itself remove newlines or control characters; handle them separately.
  • Display web logs as text, or encode them for the relevant HTML context.
  • Limit input length, for example to 256 characters, to reduce log flooding.
  • Centralize normalization in a helper such as sanitizeForLog.
  • Do not log passwords, tokens, or other secrets.

Examples

Before

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

app.get('/audit', (req, res) => {
  const agent = req.headers['user-agent']; // Untrusted input
  const q = req.query.q; // Untrusted input

  // BAD: insert input directly into the log template
  console.info(`[AUDIT] agent=${agent} query=${q}`);

  res.send('ok');
});

app.listen(3001);

After

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

function sanitizeForLog(v) {
  if (typeof v !== 'string') return v;
  // Remove CR/LF and Unicode line separators; limit length
  const noCtl = v.replace(/[\r\n]/g, '').replace(/[\u2028\u2029]/g, '');
  return noCtl.length > 256 ? noCtl.slice(0, 256) + '…' : noCtl;
}

app.get('/audit', (req, res) => {
  const rawAgent = req.headers['user-agent'];
  const rawQ = req.query.q;
  const agent = sanitizeForLog(String(rawAgent || 'unknown'));
  const q = sanitizeForLog(String(rawQ || ''));

  // GOOD: use a fixed format with normalized values
  console.info('[AUDIT] agent=%s query=%s', agent, q);

  res.send('ok');
});

app.listen(3001);

Explanation:

  • Before: Input (agent, q) is inserted directly into the message. Newlines, HTML, or ANSI escapes may distort records and interfere with analysis.
  • After: sanitizeForLog removes CR/LF and Unicode line separators and limits length. It does not remove every control character or HTML. Together with a fixed format, this reduces line-based forgery. Terminal ANSI handling and web-viewer output encoding remain separate requirements.

References