Email Injection in Express
How Email Injection Manifests in Express
Email injection in Express applications typically occurs when user input is incorporated into email headers or body without proper validation. This vulnerability allows attackers to manipulate email headers, send emails to unintended recipients, or inject malicious content.
The most common Express-specific scenarios involve:
- Express middleware like
body-parserorexpress.urlencoded()that automatically parses form data - Template engines (Pug, EJS, Handlebars) that might inadvertently include user input in email contexts
- Express route handlers that accept form submissions and directly use the data in email functions
Here's a vulnerable Express pattern:
const express = require('express');
const app = express();
const nodemailer = require('nodemailer');
app.use(express.urlencoded({ extended: true }));
app.post('/contact', (req, res) => {
const { to, subject, message } = req.body;
// VULNERABLE: No validation of email headers
const transporter = nodemailer.createTransport({
host: 'smtp.example.com',
port: 587,
secure: false
});
transporter.sendMail({
from: '[email protected]',
to: to, // User-controlled
subject: subject, // User-controlled
text: message // User-controlled
}).then(() => {
res.send('Email sent');
}).catch(err => {
res.status(500).send('Error sending email');
});
});An attacker could exploit this by submitting:
to: [email protected]\r\nCc: [email protected],[email protected]\r\nBcc: [email protected]\r\nSubject: \r\n
Message: Actual message hereThis would send the email to the intended recipient AND the CC/BCC addresses, potentially spamming multiple users. The CRLF (\r\n) injection allows adding new headers.
Another Express-specific scenario involves template rendering:
app.post('/newsletter', (req, res) => {
const { email, subject, content } = req.body;
// VULNERABLE: User input in template context
const html = template.render({
email: email,
subject: subject,
content: content
});
// Later used in email without validation
sendNewsletter(html);
});Express middleware can also introduce risks when combined with email functionality. For example, using express-validator incorrectly:
const { body, validationResult } = require('express-validator');
app.post('/contact', [
body('email').isEmail(),
body('subject').notEmpty()
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// VULNERABLE: Validation passes but doesn't sanitize headers
sendEmail(req.body.email, req.body.subject, req.body.message);
});The validation checks format but doesn't prevent header injection if an attacker finds a way to bypass the checks.
Express-Specific Detection
Detecting email injection in Express applications requires both static analysis and runtime scanning. middleBrick's API security scanner can identify these vulnerabilities through black-box testing of your Express endpoints.
For manual detection in your Express codebase, look for these patterns:
# Search for potentially vulnerable patterns
grep -r "req.body" --include="*.js" --include="*.ts" | grep -E "(sendMail|mail|email)"middleBrick scans Express applications by:
- Testing form endpoints with crafted payloads containing CRLF sequences
- Analyzing OpenAPI specs (if available) for parameters that might be used in email contexts
- Checking for insufficient input validation on POST endpoints that handle contact forms, newsletters, or user messages
- Identifying endpoints that accept email addresses, subjects, or message bodies without proper sanitization
Here's how middleBrick reports email injection risks:
{
"endpoint": "/api/contact",
"method": "POST",
"risk": "HIGH",
"category": "Input Validation",
"finding": "Potential email header injection via unvalidated user input",
"remediation": "Validate and sanitize all email-related inputs, use email libraries that automatically handle header encoding",
"evidence": "CRLF injection successful in subject field"
}For Express applications with OpenAPI specifications, middleBrick performs enhanced analysis:
paths:
/contact:
post:
parameters:
- name: to
in: formData
schema:
type: string
# middleBrick flags this as high risk if no validation is specified
middleBrick's LLM security scanning also detects if your Express app has AI-powered email features that might be vulnerable to prompt injection attacks that could lead to email manipulation.
Runtime detection in development can be enhanced with middleware:
const emailInjectionMiddleware = (req, res, next) => {
if (req.method === 'POST' && req.headers['content-type']?.includes('form')) {
const suspicious = ['\r', '\n', '%0A', '%0D']
.some(char => Object.values(req.body).some(val => val.includes(char)));
if (suspicious) {
console.warn('Potential email injection attempt detected:', req.body);
return res.status(400).json({ error: 'Invalid input detected' });
}
}
next();
};
app.use(emailInjectionMiddleware);Express-Specific Remediation
Fixing email injection in Express requires a combination of input validation, output encoding, and using secure email libraries. Here are Express-specific remediation strategies:
First, implement strict input validation using express-validator:
const { body, validationResult } = require('express-validator');
const emailValidators = [
body('to')
.isEmail()
.withMessage('Valid recipient email required')
.bail(),
body('subject')
.trim()
.isLength({ max: 200 })
.withMessage('Subject too long')
.matches(/^[^\x00-\x1F\x7F]+$/)
.withMessage('Subject contains invalid characters'),
body('message')
.trim()
.isLength({ max: 10000 })
.withMessage('Message too long')
.matches(/^[^\x00-\x1F\x7F]+$/)
.withMessage('Message contains invalid characters')
];The regex pattern ^[^\x00-\x1F\x7F]+$ blocks control characters including CRLF sequences.
Next, use email libraries that automatically handle header encoding:
const nodemailer = require('nodemailer');
app.post('/contact', emailValidators, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { to, subject, message } = req.body;
// Use nodemailer's built-in safety features
const transporter = nodemailer.createTransport(smtpConfig);
transporter.sendMail({
from: process.env.EMAIL_FROM,
to: to,
subject: subject,
text: message,
headers: {
'X-Application': 'Contact Form',
'X-Validation': 'Passed'
}
}).then(() => {
res.json({ success: true });
}).catch(err => {
console.error('Email sending failed:', err);
res.status(500).json({ error: 'Email sending failed' });
});
});For Express applications using template engines, ensure user input is properly escaped:
// Pug template with automatic escaping
const template = pug.compileFile('email-template.pug');
app.post('/newsletter', emailValidators, (req, res) => {
const { to, subject, content } = req.body;
const html = template({
email: to,
subject: subject,
content: content
});
// The template engine automatically escapes dangerous characters
sendNewsletter(to, subject, html);
});Create a reusable email sanitizer middleware:
const emailSanitizer = (req, res, next) => {
const sanitize = (value) => {
if (typeof value !== 'string') return value;
// Remove all control characters except newline in message body
if (req.path.includes('/email')) {
return value.replace(/[^\x20-\x7E\x0A\x0D]/g, '');
}
// For headers, remove all control characters
return value.replace(/[\x00-\x1F\x7F]/g, '');
};
if (req.body && typeof req.body === 'object') {
req.body = JSON.parse(JSON.stringify(req.body, (key, value) => {
return sanitize(value);
}));
}
next();
};
app.post('/contact', emailSanitizer, emailValidators, contactHandler);For Express apps with TypeScript, add compile-time safety:
interface EmailPayload {
to: string;
subject: string;
message: string;
}
const validateEmailPayload = (payload: any): payload is EmailPayload => {
return typeof payload.to === 'string' &&
typeof payload.subject === 'string' &&
typeof payload.message === 'string';
};
app.post('/contact', (req: Request, res: Response) => {
if (!validateEmailPayload(req.body)) {
return res.status(400).json({ error: 'Invalid payload' });
}
// TypeScript guarantees the shape at this point
const { to, subject, message } = req.body;
// ... continue with secure sending
});