# PYTHON-DJANGO-SEC-061: Django XSS in send_mail html_message Parameter

> **Severity:** MEDIUM | **CWE:** CWE-79 | **OWASP:** A03:2021

- **Language:** Python
- **Category:** Django
- **URL:** https://codepathfinder.dev/registry/python/django/PYTHON-DJANGO-SEC-061
- **Detection:** `pathfinder scan --ruleset python/PYTHON-DJANGO-SEC-061 --project .`

## Description

This rule detects HTML injection vulnerabilities in Django applications where untrusted
user input from HTTP request parameters flows into the html_message keyword argument
of Django's send_mail() function without proper sanitization.

Django's send_mail() accepts an optional html_message parameter for sending HTML
email alongside a plain text alternative. When user-controlled data is incorporated
into the html_message string without HTML escaping, attackers can inject HTML tags,
attributes, and potentially JavaScript that renders when the email recipient opens it.

This is functionally the same vulnerability as SEC-060 (EmailMessage HTML injection)
but targeting the send_mail() API, which is the most commonly used Django email
function due to its simpler interface. The send_mail() function is frequently called
from Django views that process form submissions, making it a common location for
user input to reach HTML email content.


## Vulnerable Code

```python
from django.core.mail import EmailMessage, send_mail

    body = request.POST.get('message')
    email = EmailMessage("Subject", body, "from@test.com", ["to@test.com"])
    email.content_subtype = "html"
    email.send()


# SEC-061: XSS in send_mail html_message
    content = request.POST.get('body')
    send_mail("Subject", "text body", "from@test.com", ["to@test.com"],
              html_message=content)
```

## Secure Code

```python
from django.core.mail import send_mail
from django.utils.html import escape, strip_tags
from django.template.loader import render_to_string

def contact_form(request):
    name = request.POST.get('name', '')
    message = request.POST.get('message', '')
    email = request.POST.get('email', '')
    # SECURE OPTION 1: Use Django template for html_message
    html_message = render_to_string('emails/contact.html', {
        'name': name,      # Auto-escaped as {{ name }} in template
        'message': message, # Auto-escaped as {{ message }} in template
    })
    send_mail(
        subject=f'Contact from {name}',
        message=strip_tags(html_message),  # Plain text fallback
        from_email='noreply@example.com',
        recipient_list=['support@example.com'],
        html_message=html_message,
    )

def welcome_email(request):
    username = request.POST.get('username', '')
    # SECURE OPTION 2: Escape user input explicitly
    escaped_name = escape(username)
    html_message = f'<h1>Welcome, {escaped_name}!</h1><p>Your account is ready.</p>'
    send_mail(
        subject='Welcome to MyApp',
        message=f'Welcome, {username}! Your account is ready.',
        from_email='noreply@example.com',
        recipient_list=[request.POST.get('email', '')],
        html_message=html_message,
    )

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, flows, QueryType
from codepathfinder.presets import PropagationPresets

_DJANGO_SOURCES = [
    calls("request.GET.get"),
    calls("request.POST.get"),
    calls("request.GET"),
    calls("request.POST"),
    calls("request.COOKIES.get"),
    calls("request.FILES.get"),
    calls("*.GET.get"),
    calls("*.POST.get"),
]


@python_rule(
    id="PYTHON-DJANGO-SEC-061",
    name="Django XSS in send_mail html_message",
    severity="MEDIUM",
    category="django",
    cwe="CWE-79",
    tags="python,django,xss,send-mail,html,OWASP-A03,CWE-79",
    message="User input in send_mail() html_message parameter. Sanitize content.",
    owasp="A03:2021",
)
def detect_django_sendmail_xss():
    """Detects user input flowing into send_mail() html_message."""
    return flows(
        from_sources=_DJANGO_SOURCES,
        to_sinks=[
            calls("send_mail"),
            calls("django.core.mail.send_mail"),
        ],
        sanitized_by=[
            calls("escape"),
            calls("strip_tags"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Generate html_message content using render_to_string() with a template file; Django templates auto-escape all {{ variable }} values.
- When constructing html_message as a Python string, escape all user-controlled values with django.utils.html.escape() before inclusion.
- Generate the plain text message argument from the HTML by using strip_tags() on the escaped HTML content.
- Validate the email recipient address with Django's EmailValidator before passing it to send_mail().
- For emails triggered by user-submitted forms, consider whether including the raw user content in the HTML body is necessary, or whether a sanitized summary is sufficient.

## Security Implications

- **Phishing Content Injection into Legitimate Emails:** send_mail() is widely used for transactional emails (password resets, welcome
emails, order confirmations). An attacker who controls input to any of these
flows can inject fake instructions, deceptive links, or fraudulent content
into emails that recipients trust because they originate from the legitimate
application domain.

- **Contact Form Abuse for Phishing:** Contact forms that send submitted content in HTML emails are a particularly
common injection vector. An attacker submits HTML content through the contact
form, which is then included in the html_message parameter and delivered to
the recipient's inbox. The recipient sees what appears to be a normal contact
submission but containing attacker-controlled HTML.

- **Password Reset Flow Manipulation:** If user-controlled data (such as a submitted username or display name) flows
into the html_message of a password reset email, attackers can inject content
that makes the email appear to contain different reset instructions, a malicious
link, or a fake customer service message.

- **Notification Email Compromise:** Applications that send HTML notifications containing user-supplied content
(comments, messages, names) without escaping allow attackers to distribute
malicious HTML to other users through the application's email system,
potentially at scale.


## FAQ

**Q: How does this differ from PYTHON-DJANGO-SEC-060 (EmailMessage)?**

SEC-060 targets Django's lower-level EmailMessage and EmailMultiAlternatives
classes. SEC-061 targets the higher-level send_mail() convenience function which
is more commonly used in Django views for transactional emails. Both are HTML
injection sinks but require different sink patterns to detect. Applications
typically use send_mail() for simple cases and EmailMessage for more control.


**Q: Does using html_message=render_to_string(...) eliminate this finding?**

Yes, if the render_to_string() template properly uses {{ variable }} interpolation
for user-controlled values, which auto-escapes them. The finding is eliminated
because the sanitizer (render_to_string with auto-escaping) is recognized as
a safe transformation of user input before it reaches the html_message argument.


**Q: Can the subject parameter of send_mail() also be injected?**

Email header injection (CWE-93) through the subject parameter is a separate
concern from HTML injection in the body. Django's send_mail() sanitizes the
subject parameter against header injection. However, you should still validate
that the subject contains reasonable content and does not include newlines.


**Q: Is it safe to include the user's email address in the html_message?**

Email addresses should be escaped if included in HTML. An email address can
contain characters like +, ., and @ that are not HTML-special, but a malformed
or crafted email address might contain <, >, or & characters. Apply escape()
to any value included in html_message content, including email addresses.


**Q: Our contact form sends user messages as HTML. Is that pattern safe with proper escaping?**

Yes. If you escape the user's submitted message with escape() before including
it in html_message, the HTML injection risk is eliminated. The escaped text
renders as visible characters rather than as HTML markup. For contact forms,
consider whether HTML rendering is necessary at all -- plain text emails avoid
the escaping requirement entirely.


**Q: How do we test that our send_mail() calls are safe against HTML injection?**

Write a test that submits a value containing '<script>alert(1)</script>' through
the form and verifies that the outgoing email (using Django's mail.outbox in
tests) contains '&lt;script&gt;alert(1)&lt;/script&gt;' rather than the raw
script tag in the html_message. This directly verifies that escaping is applied.


## References

- [CWE-79: Cross-site Scripting](https://cwe.mitre.org/data/definitions/79.html)
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
- [Django send_mail() documentation](https://docs.djangoproject.com/en/stable/topics/email/#send-mail)
- [Django Sending HTML Email](https://docs.djangoproject.com/en/stable/topics/email/#sending-html-email)
- [Django Template render_to_string()](https://docs.djangoproject.com/en/stable/topics/templates/#django.template.loader.render_to_string)
- [OWASP HTML Injection](https://owasp.org/www-community/attacks/HTML_Injection)

---

Source: https://codepathfinder.dev/registry/python/django/PYTHON-DJANGO-SEC-061
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
