Email Injection in Django with Firestore
Email Injection in Django with Firestore — how this specific combination creates or exposes the vulnerability
Email injection occurs when user-controlled data is placed into email headers without validation, enabling attackers to inject additional headers such as CC, BCC, or extra recipients. In Django applications that use Google Cloud Firestore as a backend, the risk emerges from two layers: Django’s email-sending mechanisms and the Firestore data store that persists user-supplied values used in those messages.
Django’s send_mail and related email APIs rely on Python’s email module. If a developer directly uses user input in header construction—such as assigning a request parameter to to, cc, or subject—and then passes that to Firestore for logging or retrieval, stored values can later be reused in email construction, creating a multi-step injection path. For example, an attacker might supply a payload like test@example.com\r\nCC: spam@example.com during registration; Firestore stores the raw string, and when an admin export or notification job reads that document and feeds it into send_mail, the injected header is interpreted by the email backend, potentially redirecting the message.
Because Firestore does not parse or sanitize string content, it preserves line-break characters and header-like sequences. If a Django view retrieves a document field and uses it in an email header without sanitization, the stored payload becomes a vector. Additionally, Firestore client libraries in Python do not inherently validate or escape content; they pass strings through. This means the framework’s email utilities and Firestore’s persistence behavior must both be considered when assessing risk.
Common patterns that increase exposure include logging raw user input into Firestore fields intended for audit trails, using document fields as templates for email content, and constructing dynamic recipients or subjects from stored values. Without strict input validation and output encoding at both the application and data layers, what originates as a simple string in Firestore can become a functional injection when consumed by Django’s email subsystem.
Firestore-Specific Remediation in Django — concrete code fixes
Mitigation requires validating and sanitizing data at the point of entry, avoiding direct concatenation of user input into email headers, and ensuring Firestore-stored values are treated as opaque data. Below are concrete, Firestore-aware examples for Django.
1. Validate and sanitize before storing to Firestore
Use Django forms or serializers to enforce rules on email fields. Normalize and reject suspicious input before it reaches Firestore.
import re
from django import forms
from google.cloud import firestore
class ContactForm(forms.Form):
email = forms.EmailField()
def submit_contact(data):
form = ContactForm(data)
if form.is_valid():
db = firestore.Client()
doc_ref = db.collection('contacts').document()
# Store only the normalized email
doc_ref.set({
'email': form.cleaned_data['email'],
'name': data.get('name', '')
})
return True
return False
2. Safe email construction using Django utilities
When sending emails, avoid using raw Firestore fields in headers. Use Django’s EmailMessage with explicit arguments and sanitize any content that will be displayed in the message body.
from django.core.mail import EmailMessage
from google.cloud import firestore
import html
def notify_admin(user_id):
db = firestore.Client()
doc = db.collection('users').document(user_id).get()
if doc.exists:
user_data = doc.to_dict()
subject = 'Account update'
# Escape any user-controlled content intended for the body
body = f'User: {html.escape(user_data.get("email", ""))} has updated their profile.'
msg = EmailMessage(
subject=subject,
body=body,
from_email='noreply@example.com',
to=['admin@example.com'],
)
msg.content_subtype = 'plain'
msg.send()
3. Reject or encode line characters in stored strings
Remove or replace carriage return and line feed characters in fields that may be reused in email contexts. This prevents stored values from breaking header structure when later interpolated.
def sanitize_for_email(value: str) -> str:
# Remove CR/LF to prevent header injection
return value.replace('\r', '').replace('\n', '')
def store_user_preference(user_id, raw_input):
clean_value = sanitize_for_email(raw_input)
db = firestore.Client()
db.collection('preferences').document(user_id).set({
'display_name': clean_value
}, merge=True)
4. Principle of least privilege and monitoring
Ensure the Firestore credentials used by Django have minimal write/read scope for the collections involved. Combine this with application-level logging of outbound email metadata (without message content) to detect anomalies such as unexpected recipient patterns.
5. Template-based rendering for complex messages
If email templates are necessary, use Django’s template engine with autoescaping rather than string interpolation, and keep dynamic data separate from header structures.
from django.template.loader import render_to_string
from django.core.mail import EmailMultiAlternatives
def send_templated_welcome(email, name):
subject = 'Welcome'
html_content = render_to_string('welcome_email.html', {
'name': name,
'email': email
})
msg = EmailMultiAlternatives(
subject=subject,
body='',
from_email='noreply@example.com',
to=[email],
)
msg.attach_alternative(html_content, 'text/html')
msg.send()