Email Injection in Django with Mutual Tls
Email Injection in Django with Mutual TLS — how this specific combination creates or exposes the vulnerability
Email Injection is a class of injection vulnerability where attacker-controlled data is inserted into email headers or the envelope (e.g., TO, CC, FROM, REPLY-TO), causing unintended recipients, header smuggling, or server-side request forgery. In Django, this commonly arises when user input is passed directly to email-sending APIs such as send_mail or EmailMessage without strict validation or sanitization. Typical vulnerable patterns include using raw user input in the subject line or message headers, or constructing email addresses via string concatenation.
Mutual TLS (mTLS) changes the trust boundaries of the request but does not inherently protect against Email Injection. When mTLS is enforced, the server validates the client certificate, which ensures the identity of the connecting client or upstream service. However, the application layer remains responsible for sanitizing inputs that become email headers. If Django uses mTLS only to authenticate the caller (for example, an internal microservice or API client) and that caller provides data that ends up in email headers, the mTLS channel does nothing to prevent header injection. In some deployments, mTLS can indirectly increase risk surface: services that previously relied on network segregation now accept authenticated HTTPS requests and may inadvertently pass authenticated user-supplied fields—such as a display name or email address—into email-sending logic without adequate escaping.
Attackers can exploit Email Injection to perform header smuggling (e.g., injecting additional headers like Bcc or Cc), conduct phishing via spoofed FROM addresses, or trigger SSRF by embedding data into mailto: URIs handled by the server. In Django, common vulnerable code looks like:
from django.core.mail import send_mail
# Vulnerable: user_display_name and user_email are untrusted
send_mail(
subject=f'Hello {user_display_name}',
message='...',
from_email='no-reply@example.com',
recipient_list=[f'{user_display_name} <{user_email}>'],
)
If user_display_name contains newline characters or special header characters, it can inject additional headers or break out of the header value. Even with mTLS ensuring the request originates from a trusted client, the application must treat all inputs as untrusted.
Django’s EmailMessage and send_mail ultimately rely on Python’s email.message and smtplib. These libraries perform limited header sanitization; they do not automatically neutralize newline sequences (\r, \n) or prevent header folding. Therefore, even when mTLS is used for transport-level identity assurance, developers must explicitly validate and encode header data to prevent injection.
Mutual TLS-Specific Remediation in Django — concrete code fixes
To remediate Email Injection in Django while using mTLS, treat email header construction as untrusted input handling regardless of transport security. Apply strict validation, canonicalization, and encoding to any user-controlled data that reaches email headers. Below are concrete, safe patterns.
1. Validate and encode email headers
Use Django’s built-in email utilities and the standard library’s email.header to encode non-ASCII and special characters safely. Avoid string concatenation for header assembly.
from django.core.mail import EmailMessage
from email.header import Header
from email.utils import formataddr
# Safe: validate email with a simple regex or Django validators
import re
email_regex = re.compile(r'^[^\s@]+@[^\s@]+\.[^\s@]+$')
def safe_send_welcome(user_email, user_name):
if not email_regex.match(user_email):
raise ValueError('Invalid email address')
# encode non-ascii display names
display = str(Header(user_name, 'utf-8'))
addr = formataddr((display, user_email))
msg = EmailMessage(
subject='Welcome',
body='Thank you for joining.',
from_email='no-reply@example.com',
to=[addr],
)
msg.content_subtype = 'html'
msg.send()
2. Use Django’s send_mail with careful escaping
If you prefer send_mail, ensure the recipient list contains properly formatted addresses and avoid injecting display names with raw input. For simple cases, omit the display name and rely on the verified email address only.
from django.core.mail import send_mail
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
def send_simple_mail(user_email):
validate_email(user_email) # raises ValidationError if invalid
send_mail(
subject='Welcome',
message='Thank you for joining.',
from_email='no-reply@example.com',
recipient_list=[user_email],
)
3. Enforce mTLS at the server/load balancer and keep application-layer validation
Configure your web server or reverse proxy to require client certificates. In Django, you can still read the certificate subject for audit logging, but never rely on it to sanitize email headers. Example using a middleware to log mTLS identity without affecting header safety:
import ssl
class MutualTlsLoggingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
cert = request.META.get('SSL_CLIENT_CERT')
if cert:
# Log certificate details for auditing; do not use for header decisions
request.audit_subject = ssl.DER_cert_to_PEM_cert(cert)
response = self.get_response(request)
return response
4. Use environment-based defaults and avoid dynamic FROM addresses
Configure FROM_EMAIL from environment variables and avoid building FROM from user input. This prevents spoofed FROM headers even when mTLS is in use.
import os
from django.conf import settings
DEFAULT_FROM_EMAIL = os.getenv('DJANGO_DEFAULT_FROM_EMAIL', 'noreply@example.com')
# In settings.py or a secure config module:
# DEFAULT_FROM_EMAIL = 'noreply@example.com'
By combining transport-level mTLS with disciplined header encoding and validation, you reduce the likelihood of Email Injection while preserving the identity assurance that mTLS provides.