Email Injection in Feathersjs with Basic Auth
Email Injection in Feathersjs with Basic Auth — how this specific combination creates or exposes the vulnerability
Email Injection occurs when user-controlled input is improperly handled in email-related headers or commands, enabling attackers to inject additional headers such as CC, BCC, or injected content into the message body. In Feathersjs, services that send email (for example via a transport like SMTP or an email provider SDK) may build messages using user-supplied data without strict validation or sanitization. When Basic Auth is used for service-level concerns such as database connections, external API calls, or transport configuration, the interaction between credential handling and email composition can expose injection risks.
Consider a Feathersjs service that accepts user input for recipient, subject, and message body, and then passes these to an email transport. If the service also relies on Basic Auth credentials (username and password) to authenticate to an external mail relay or a backend mail service, misconfiguration can occur. For example, if the transport or a middleware helper uses Basic Auth credentials to construct or sign email headers, or if the credentials are logged or reflected in error messages, an attacker may manipulate input fields to alter the email routing or inject malicious headers. In worst cases, injected headers can redirect email delivery, expose authentication material in verbose errors, or facilitate phishing via compromised header values.
Feathersjs typically uses transports such as nodemailer, and misconfigured hooks or services that interpolate user input into header fields (e.g., cc, bcc, inReplyTo, or custom headers) can become injection vectors. Basic Auth used for transport authentication does not inherently protect against injection in the application layer; if the code does not validate or sanitize data before it reaches the transport, the credentials and the email composition remain independently vulnerable. Attack patterns include header splitting with newline characters (\r\n) to inject additional headers, and manipulation of fields that the transport trusts. Because Feathersjs encourages a service-oriented architecture, developers must ensure each service validates and encodes data before it is used in email construction, regardless of how transport authentication is configured.
Basic Auth-Specific Remediation in Feathersjs — concrete code fixes
To mitigate Email Injection in Feathersjs when using Basic Auth for transport or service authentication, focus on input validation, strict header encoding, and isolating credentials from user-controlled data. Below are concrete remediation steps and code examples.
1. Validate and sanitize email headers and user input
Never directly use user input in email headers. Use a dedicated validation library and explicitly allowlist safe characters. For addresses, use a robust email validation routine and avoid concatenating user input into header values.
// src/hooks/validate-email-input.js
const validator = require('validator');
module.exports = function validateEmailInput() {
return async context => {
const { email, cc, bcc } = context.data || {};
if (email && !validator.isEmail(email)) {
throw new Error('Invalid email address');
}
if (cc && !validator.isEmail(cc)) {
throw new Error('Invalid CC email address');
}
if (bcc && !validator.isEmail(bcc)) {
throw new Error('Invalid BCC email address');
}
// Remove or encode any newline/carriage-return characters
const clean = (str) => str.replace(/[\r\n]+/g, '');
context.data.cc = cc ? clean(cc) : undefined;
context.data.bcc = bcc ? clean(cc) : undefined;
return context;
};
};
2. Use nodemailer with secure transport options and avoid header interpolation
Configure nodemailer transports so credentials are provided as separate options and never built into user-controlled strings or headers. This keeps Basic Auth credentials out of the email composition path.
// src/services/email.service.js
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
secure: false,
auth: {
user: process.env.SMTP_USER, // Basic Auth username
pass: process.env.SMTP_PASS // Basic Auth password
}
});
async function sendMail({ to, subject, text, cc, bcc }) {
const mailOptions = {
from: process.env.EMAIL_FROM,
to,
subject,
text,
// Never interpolate user input into headers
cc: cc || undefined,
bcc: bcc || undefined,
// Avoid custom headers from untrusted sources; if required, validate strictly
headers: {
'X-App-Version': '1.0.0'
}
};
await transporter.sendMail(mailOptions);
}
module.exports = sendMail;
3. Secure hooks and service methods to prevent injection
Add hooks that sanitize and explicitly set headers rather than merging raw user data. Ensure that any custom headers are validated and that newline characters are stripped to prevent header splitting.
// src/hooks/secure-email-hooks.js
const validator = require('validator');
const stripNewLines = (value) => value.replace(/[\r\n]+/g, '');
module.exports = function secureEmailHooks() {
return {
before: {
create: async context => {
const { to, subject, body, customHeaders } = context.data || {};
if (!validator.isEmail(to)) {
throw new Error('Invalid recipient');
}
// Validate subject safely (limit length, disallow control chars)
context.data.subject = subject ? subject.substring(0, 200).replace(/[\r\n]+/g, '') : '';
context.data.body = body ? body : '';
// If customHeaders are provided, only allow known safe keys
if (customHeaders) {
const safeHeaders = {};
Object.keys(customHeaders).forEach(key => {
const safeKey = stripNewLines(key);
const safeValue = stripNewLines(customHeaders[key]);
if (['X-Request-ID', 'X-Correlation-ID'].includes(safeKey)) {
safeHeaders[safeKey] = safeValue;
}
});
context.data.customHeaders = safeHeaders;
}
return context;
}
}
};
};
4. Apply the validation hook to your email service
// src/services/email/index.js
const sendMail = require('./email.service');
const validateEmailInput = require('./hooks/validate-email-input');
const secureEmailHooks = require('./hooks/secure-email-hooks');
module.exports = function() {
const app = this;
app.service('email').hooks({
before: {
create: [validateEmailInput(), secureEmailHooks()]
}
});
app.service('email').configure(() => {
// Example usage within a method or route handler
app.service('email').create({
to: 'recipient@example.com',
subject: 'Hello',
body: 'This is the email body',
cc: 'copy@example.com',
bcc: 'blind@example.com'
}).then(() => console.log('Email sent'));
});
};