Email Injection in Flask
How Email Injection Manifests in Flask
Email Injection (CWE-93) occurs when an application incorporates untrusted user input into email headers without proper validation, allowing attackers to inject arbitrary SMTP commands via CRLF sequences. In Flask APIs that trigger email notifications—such as contact forms, password resets, or alert systems—this vulnerability can lead to spam, phishing, data exfiltration, or even remote code execution in poorly configured mail servers.
Flask's typical email-sending patterns create specific attack surfaces. Consider a common implementation using Python's smtplib:
from flask import Flask, request
import smtplib
from email.mime.text import MIMEText
app = Flask(__name__)
@app.route('/api/contact', methods=['POST'])
def contact():
user_email = request.form.get('email')
message = request.form.get('message')
msg = MIMEText(message)
msg['Subject'] = 'Contact Form Submission'
msg['From'] = '[email protected]'
msg['To'] = user_email # Vulnerable: user input in header
server = smtplib.SMTP('localhost')
server.sendmail('[email protected]', [user_email], msg.as_string())
server.quit()
return 'Sent'
An attacker submits [email protected]%0d%0aBCC:[email protected] (URL-encoded CRLF). The resulting headers become:
To: [email protected]
BCC: [email protected]
Subject: Contact Form Submission
From: [email protected]
[message body]
The injected BCC header causes the email to be surreptitiously sent to [email protected]. Worse, if the attacker controls the Subject or From fields (e.g., via a subject parameter), they could spoof emails or inject additional headers like Content-Type to manipulate message body interpretation (OWASP A03:2021 – Injection).
Flask extensions like Flask-Mail exhibit similar risks when user input populates header fields:
from flask_mail import Mail, Message
mail = Mail(app)
@app.route('/api/notify', methods=['POST'])
def notify():
recipient = request.json.get('email')
subject = request.json.get('subject', 'Alert')
msg = Message(
subject=subject, # Vulnerable if user-controlled
sender='[email protected]',
recipients=[recipient] # Vulnerable
)
msg.body = request.json.get('body')
mail.send(msg)
return 'Sent'
Here, both recipient and subject are injection points. Even if recipients is a list, a single string containing CRLF can break header parsing. Real-world CVEs like CVE-2014-1930 (related to email header injection in Python libraries) demonstrate the severity: attackers can execute arbitrary commands via sendmail wrappers if the mail server interprets injected headers.
Flask-Specific Detection
Identifying Email Injection in Flask requires examining both code and runtime behavior. In code reviews, search for Flask routes that:
- Use
request.form,request.json, orrequest.argsto populate email header fields (To,From,Subject,Cc,Bcc). - Call
smtplib.SMTP.sendmail(),email.message.EmailMessage, or Flask-Mail'sMessagewith unsanitized user input. - Construct email strings via f-strings or concatenation (e.g.,
f"To: {user_input}\n").
For example, any occurrence of msg['To'] = request.form.get('email') or recipients=[request.args.get('email')] is a red flag.
Dynamic scanning with middleBrick automates detection for exposed API endpoints. As a black-box scanner, middleBrick submits payloads containing CRLF sequences (%0d%0a or literal \r\n) to parameters likely to be used in email contexts (e.g., email, to, subject). It then analyzes responses for:
- Header reflection: If the API returns email headers in the response (e.g., debugging info), injected headers may appear.
- Behavioral changes: If the API sends an email to a controlled address (provided during scan configuration), middleBrick inspects the received email for injected headers (e.g., unexpected
Bcc). - Error messages: SMTP errors like
553 invalid headeror Python tracebacks revealing header structure can indicate injection attempts.
For instance, scanning a Flask endpoint at /api/contact with payload [email protected]%0d%0aBcc:[email protected] might yield a report showing that the email was delivered to [email protected] despite not being in the original request, confirming Email Injection. middleBrick's scoring (0–100) assigns a high severity to such findings, mapping to OWASP A03:2021 and PCI-DSS requirement 6.5.1.
Note: Detection efficacy depends on the scanner's ability to observe email outputs. middleBrick's Pro plan includes continuous monitoring and can integrate with controlled inboxes to capture injected emails, while the CLI tool (middlebrick scan <url>) allows on-demand testing from terminal.
Flask-Specific Remediation
Remediation centers on strict validation and sanitization of any user-controlled data destined for email headers. Never trust input—even if it appears to be an email address. The core principle: reject any input containing CR (\r) or LF (\n) characters before it reaches email construction functions.
Flask's native tools (Python standard library) provide robust ways to implement this. First, create a validation helper:
import re
from flask import abort
def validate_header_value(value: str, field_name: str) -> str:
"""Reject CR/LF in header values; return sanitized value if safe.
"""
if not isinstance(value, str):
abort(400, f"{field_name} must be a string")
# Detect any CR or LF
if '\r' in value or '\n' in value:
abort(400, f"Invalid {field_name}: newlines not allowed")
# Optional: additionally validate email format for email fields
if field_name in ('email', 'to', 'from', 'cc', 'bcc'):
# Simple regex for demonstration; use email-validator in production
if not re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', value):
abort(400, f"Invalid {field_name} format")
return value.strip()
Apply this to every header field in your Flask routes:
from flask import Flask, request
import smtplib
from email.mime.text import MIMEText
app = Flask(__name__)
@app.route('/api/contact', methods=['POST'])
def contact():
# Extract and validate
user_email = validate_header_value(request.form.get('email', ''), 'email')
subject = validate_header_value(request.form.get('subject', 'Contact Form'), 'subject')
message = request.form.get('message', '')
# Construct email safely
msg = MIMEText(message)
msg['Subject'] = subject
msg['From'] = '[email protected]' # Static, safe
msg['To'] = user_email
# Send
server = smtplib.SMTP('localhost')
server.sendmail('[email protected]', [user_email], msg.as_string())
server.quit()
return 'Sent'
For Flask-Mail, validate before passing to Message:
from flask_mail import Mail, Message
mail = Mail(app)
@app.route('/api/notify', methods=['POST'])
def notify():
recipient = validate_header_value(request.json.get('email'), 'email')
subject = validate_header_value(request.json.get('subject', 'Alert'), 'subject')
msg = Message(
subject=subject,
sender='[email protected]', # Static
recipients=[recipient]
)
msg.body = request.json.get('body', '')
mail.send(msg)
return 'Sent'
Key points:
- Reject, don't sanitize: Stripping CR/LF might still allow bypasses (e.g., using encoded characters). Abort the request on detection.
- Validate all headers: Even if a header is static (like
From), ensure any dynamic portion (e.g.,reply-to) is validated. - Use allowlists: For fields like
subject, consider restricting to a set of safe values if possible. - Email format validation: Use the
email-validatorlibrary (not native, but recommended) for robust email parsing. Flask itself lacks built-in email validation, so the regex above is minimal; in production, installemail-validatorand callvalidate_email(user_email).
These fixes ensure that user input cannot break out of header contexts. Combined with middleBrick's scanning—integrated via GitHub Action to fail builds on detection—you can enforce these controls continuously.
Frequently Asked Questions
What is Email Injection in Flask?
How can I prevent Email Injection in my Flask API?
validate_header_value() shown above, and apply it to every header field (To, From, Subject, etc.). Additionally, validate email formats with a library like email-validator, and never concatenate user input into email strings. Scan your APIs regularly with middleBrick to catch regressions.