MCP Security · Observability

MCP server structured logging security: log injection and security event schema

Your MCP server's audit log is only as trustworthy as the way it serializes user-controlled data. An attacker who can inject newlines, braces, or quotes into a log field can spoof entire log entries — adding fake success events, masking real errors, or poisoning the audit trail that security teams rely on during incident response. Structured JSON logging prevents injection; a complete security event schema gives reviewers and SkillAudit something meaningful to verify.

How log injection works in MCP servers

Log injection is possible whenever user-controlled data is concatenated into a log line rather than being serialized as a field value. In an MCP server, the attack surface is wider than typical web apps because tool arguments come from the LLM, and the LLM can be prompted to supply adversarial inputs:

// VULNERABLE: string concatenation logging
console.log(`[AUDIT] tool=read_file path=${args.path} caller=${ctx.callerId}`);

// If args.path = "../etc/passwd\n[AUDIT] tool=read_file path=.env caller=admin"
// Log output:
// [AUDIT] tool=read_file path=../etc/passwd
// [AUDIT] tool=read_file path=.env caller=admin

// The second line is fake — but a SIEM parsing this file can't tell.

The attacker's goal: insert a fake log entry that makes an attack look authorized, or delete the real entry by injecting a carriage return that overwrites the previous line in some log parsers.

Why JSON (NDJSON) logging prevents injection

JSON serialization encodes all special characters. A newline in a field value becomes \n, a closing brace becomes \}, and the JSON parser on the receiving end sees the original value, not the attacker's injected structure:

// SECURE: structured JSON (using pino, winston, or similar)
import pino from 'pino';
const logger = pino({ level: 'info' });

// Same malicious input — safe in structured JSON
logger.info({
  event: 'tool_call',
  tool: 'read_file',
  path: args.path,  // "../etc/passwd\n[AUDIT] tool=read_file path=.env caller=admin"
  callerId: ctx.callerId,
});

// Output (one line of NDJSON):
// {"level":30,"time":1718290812345,"event":"tool_call","tool":"read_file","path":"../etc/passwd\\n[AUDIT] tool=read_file path=.env caller=admin","callerId":"user_123"}

// The injected text is encoded — it cannot create a second log entry.

Security event schema for MCP servers

A complete MCP server audit trail needs to capture enough context for incident response without logging PII or credential values. SkillAudit's audit trail analysis looks for these event types and fields:

Event typeRequired fieldsPurpose
tool_call tool, callerId, sessionId, argsHash, durationMs, success Every tool invocation — argsHash identifies the call without logging raw args that may contain PII
auth_failure tool, callerId, reason, failureType (missing_token|expired|insufficient_scope|idor) Authentication and authorization rejections — critical for detecting credential stuffing and IDOR probing
rate_limit_hit tool, callerId, windowMs, limit, current Rate limit enforcement — patterns indicate enumeration or brute-force
validation_rejection tool, field, reason, valueLength Input validation failures — high volume on specific fields indicates fuzzing
chain_guard_block blockedTool, sessionCategories, sessionCallCount, reason Tool chaining category guard blocks — indicates attempted read-to-write escalation
session_start sessionId, callerId, scopes, clientIp (hashed), userAgent Session establishment — baseline for correlating subsequent events

Avoiding credential leakage in logs

The second major logging security issue: logging tool arguments that contain secrets. MCP servers that log raw arguments may inadvertently log API keys passed as args or credentials extracted from environment variables:

const SENSITIVE_ARG_FIELDS = ['api_key', 'token', 'password', 'secret', 'authorization', 'credential'];

function sanitizeArgs(args: Record): Record {
  const sanitized: Record = {};
  for (const [key, value] of Object.entries(args)) {
    if (SENSITIVE_ARG_FIELDS.some(s => key.toLowerCase().includes(s))) {
      sanitized[key] = '[REDACTED]';
    } else if (typeof value === 'string' && value.length > 500) {
      sanitized[key] = `[truncated:${value.length}]`;  // don't log large blobs
    } else {
      sanitized[key] = value;
    }
  }
  return sanitized;
}

logger.info({
  event: 'tool_call',
  tool: toolName,
  args: sanitizeArgs(args),  // never log raw args directly
  argsHash: argsHash(args),  // hash for correlation without exposure
  callerId: ctx.session.callerId,
  sessionId: ctx.session.id,
});

Log forwarding security

Security logs that stay on the application server are lost during a compromise. Forward to an append-only external sink:

// pino-loki / pino-cloudwatch / custom transport
const transport = pino.transport({
  targets: [
    { target: 'pino/file', options: { destination: '/var/log/mcp-server/audit.ndjson' } },
    { target: 'pino-loki', options: { host: process.env.LOKI_URL, labels: { app: 'mcp-server' } } },
  ],
});
const logger = pino({ level: 'info' }, transport);

SkillAudit checks for evidence of structured logging (pino, winston, bunyan, or equivalent), a security-event schema with at minimum auth_failure and tool_call events, and argument sanitization patterns. Servers with only console.log calls receive a lower audit trail score.

SkillAudit findings for logging security

HIGHUnstructured string concatenation logging with user-controlled fields — log injection possible, fake audit entries can be created
HIGHRaw tool arguments logged without sanitization — credential fields (api_key, token, password) appear in plaintext in log output
MEDIUMNo auth_failure events logged — authentication rejections untracked, credential stuffing and IDOR probing not detectable
MEDIUMLogs written only to local disk — log data lost on container restart or node compromise; no external sink detected
LOWNo argsHash on tool_call events — individual calls cannot be correlated across sessions without logging raw (potentially sensitive) args

Run a SkillAudit to see your server's audit trail score. Static analysis detects console.log with template literal interpolation of arg fields, logger.info(rawArgs) without sanitization, and missing auth failure event handlers. Paste your GitHub URL →

Related: MCP server observability and security logging · tool chaining attack security · audit trail for SOC 2 and GDPR