Log Injection in Express
How Log Injection Manifests in Express
Log injection in Express applications occurs when untrusted user input is written directly to log files without proper sanitization, creating opportunities for attackers to manipulate log contents, inject malicious commands, or trigger information disclosure. Express's asynchronous nature and middleware architecture create specific injection points that developers must understand.
The most common attack vector is through request parameters that flow through Express's request handling pipeline. Consider this vulnerable Express route:
app.post('/api/logs', (req, res) => {
const { message, userId } = req.body;
const logEntry = `User ${userId}: ${message}`;
fs.appendFileSync('app.log', logEntry + '\n');
res.json({ success: true });
});An attacker could send: { "message": "test", "userId": "123" + process.platform === 'win32' ? ' && del /S /Q C:\' : ' && rm -rf /' }. This creates a log entry that, when parsed by log monitoring tools, could execute destructive commands.
Express middleware creates additional injection surfaces. The built-in express.json() and express.urlencoded() middleware parse incoming request bodies, but if logging middleware executes before validation, malicious payloads can reach log files:
app.use(express.json());
app.use(morgan('combined')); // Logs before validation
app.post('/api/data', (req, res) => {
// No validation here
res.json(req.body);
});Structured logging with libraries like winston or pino can be exploited when log objects contain unvalidated user data:
const logger = winston.createLogger({ transports: [new winston.transports.File({ filename: 'app.log' })] });
app.post('/api/events', (req, res) => {
logger.info('Event received', { data: req.body.eventData });
res.json({ success: true });
});If req.body.eventData contains newline characters or structured data with malicious fields, it can break log parsing or inject false log entries that appear to come from legitimate sources.
Log injection also manifests through Express's error handling. Uncaught exceptions that include user input in stack traces can leak sensitive data:
app.get('/api/unsafe', (req, res) => {
const { userId } = req.query;
// No validation, potential prototype pollution or code execution
const data = require(`./data/${userId}.json`);
res.json(data);
});When this throws an error, Express's default error handler logs the stack trace including the malicious userId parameter, potentially exposing file paths or sensitive system information.
Express-Specific Detection
Detecting log injection in Express requires examining both code patterns and runtime behavior. Static analysis can identify vulnerable code patterns, but dynamic scanning reveals how data actually flows through your application.
Code review should focus on Express-specific patterns. Look for middleware that logs request data without validation:
// Vulnerable pattern - logs raw request body
app.use((req, res, next) => {
console.log('Request:', req.body);
next();
});Search for instances where user input reaches logging functions. Tools like ESLint with custom rules can flag dangerous patterns:
// ESLint custom rule example
module.exports = {
create: function(context) {
return {
CallExpression(node) {
if (node.callee.type === 'Identifier' &&
(node.callee.name === 'console.log' || node.callee.name === 'logger.info')) {
// Check if arguments contain request properties
node.arguments.forEach(arg => {
if (arg.type === 'MemberExpression' &&
arg.object.type === 'Identifier' &&
arg.object.name === 'req') {
context.report({
node,
message: 'Logging unsanitized request data'
});
}
});
}
}
};
}
};Runtime detection with middleBrick scans your Express API endpoints for log injection vulnerabilities by testing unauthenticated attack surfaces. The scanner sends payloads containing newline characters, special formatting, and structured data to identify if user input reaches log files:
$ middlebrick scan https://yourapi.com
✅ Authentication checks passed
✅ BOLA/IDOR checks passed
❌ Log Injection detected in /api/logs endpoint
Risk Score: C (72/100)
Findings:
- Log Injection: Unsanitized user input in log entries
Severity: Medium
Recommendation: Validate and sanitize all user input before loggingThe middleBrick scanner tests specific Express patterns like JSON body parsing, URL parameter handling, and error stack trace logging. It sends payloads with embedded newlines and special characters to see if they appear in log outputs, mimicking real attacker techniques.
Integration with Express's error handling provides another detection layer. Custom error handlers can log suspicious patterns:
app.use((err, req, res, next) => {
if (err.message.includes('\n') || err.message.includes('\r')) {
console.warn('Potential log injection attempt detected:', err.message);
}
next(err);
});Monitoring log files for anomalies helps detect ongoing injection attempts. Look for unexpected log format changes, unusual timing patterns, or log entries containing suspicious characters that weren't in your application's normal output.
Express-Specific Remediation
Remediating log injection in Express applications requires a defense-in-depth approach that validates input, sanitizes output, and uses secure logging practices. Express's middleware architecture provides natural points for implementing these controls.
Input validation should occur as early as possible in the request pipeline. Use Express middleware to validate and sanitize all incoming data before it reaches logging functions:
const express = require('express');
const helmet = require('helmet');
const xss = require('xss');
const app = express();
// Security middleware first
app.use(helmet());
app.use(express.json());
// Input sanitization middleware
app.use((req, res, next) => {
// Sanitize query parameters
Object.keys(req.query).forEach(key => {
req.query[key] = xss(req.query[key]);
});
// Sanitize body (if JSON)
if (req.body && typeof req.body === 'object') {
sanitizeObject(req.body);
}
next();
});
function sanitizeObject(obj) {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
obj[key] = xss(obj[key]).replace(/[\n\r]/g, ' ');
} else if (typeof obj[key] === 'object') {
sanitizeObject(obj[key]);
}
});
}
// Safe logging middleware
app.use((req, res, next) => {
const safeLog = {
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString()
};
console.log(JSON.stringify(safeLog));
next();
});
app.post('/api/logs', (req, res) => {
const { message, userId } = req.body;
const sanitizedMessage = xss(message).replace(/[\n\r]/g, ' ');
const sanitizedUserId = xss(userId).replace(/[\n\r]/g, ' ');
const logEntry = `User ${sanitizedUserId}: ${sanitizedMessage}`;
fs.appendFileSync('app.log', logEntry + '\n');
res.json({ success: true });
});Structured logging with pino or winston provides better protection than console logging. These libraries handle special characters safely and support log levels that can filter sensitive information:
const pino = require('pino');
const logger = pino();
app.use((req, res, next) => {
logger.info({
event: 'request_start',
method: req.method,
url: req.originalUrl,
ip: req.ip
});
next();
});
app.post('/api/events', (req, res) => {
try {
const eventData = req.body.eventData;
// Validate event data structure
if (typeof eventData !== 'object' || !eventData.type) {
throw new Error('Invalid event data');
}
logger.info({
event: 'event_received',
type: eventData.type,
timestamp: new Date().toISOString()
});
res.json({ success: true });
} catch (err) {
logger.error({
event: 'error',
error: err.message,
stack: err.stack
});
res.status(400).json({ error: 'Invalid input' });
}
});Error handling in Express requires special attention. Custom error handlers should sanitize error messages before logging:
app.use((err, req, res, next) => {
// Sanitize error message for logging
const safeMessage = err.message.replace(/[\n\r]/g, ' ').substring(0, 200);
logger.error({
event: 'uncaught_error',
message: safeMessage,
stack: err.stack ? err.stack.substring(0, 1000) : undefined,
request: {
method: req.method,
url: req.originalUrl,
ip: req.ip
}
});
res.status(500).json({ error: 'Internal server error' });
});Rate limiting and request validation middleware prevent log flooding attacks that could overwhelm logging infrastructure or hide other malicious activity:
const rateLimit = require('express-rate-limit');
const logLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many log requests from this IP'
});
app.use('/api/logs', logLimiter);
// Validate log request structure
app.post('/api/logs', (req, res, next) => {
const { message, userId } = req.body;
if (!message || typeof message !== 'string' || message.length > 1000) {
return res.status(400).json({ error: 'Invalid message' });
}
if (!userId || typeof userId !== 'string' || userId.length > 50) {
return res.status(400).json({ error: 'Invalid user ID' });
}
next();
});Testing your Express application for log injection involves sending payloads with special characters and verifying they're properly handled. Use tools like curl or Postman to test endpoints:
curl -X POST https://yourapi.com/api/logs \
-H "Content-Type: application/json" \
-d '{"message": "test\nnew line", "userId": "123\r\n456"}'Verify that log files don't contain the injected newlines and that your application handles these inputs gracefully without errors or information disclosure.