# PYTHON-DJANGO-SEC-060: Django XSS in HTML Email Body via EmailMessage

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

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

## Description

This rule detects HTML injection vulnerabilities in Django applications where untrusted
user input from HTTP request parameters flows into HTML email body content constructed
for Django's EmailMessage class without proper sanitization.

When user-controlled data is included in HTML email content without escaping,
attackers can inject malicious HTML that renders when the recipient opens the email.
While modern webmail clients (Gmail, Outlook.com) strip JavaScript from email HTML
for security, HTML injection without script execution is still exploitable for
phishing attacks -- injecting fake login forms, manipulating the visual appearance
of the email, or adding malicious links that lead to credential theft.

Additionally, some desktop email clients and corporate email systems render HTML
more permissively, potentially allowing JavaScript execution. The safe approach is
to escape user input in HTML emails using the same techniques used for web output.


## 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()


    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 EmailMessage, EmailMultiAlternatives
from django.utils.html import escape
from django.template.loader import render_to_string

def send_confirmation(request):
    user_name = request.POST.get('name', '')
    user_email = request.POST.get('email', '')
    # SECURE OPTION 1: Use Django template for HTML email -- auto-escapes {{ variables }}
    html_body = render_to_string('emails/confirmation.html', {
        'name': user_name,  # Auto-escaped in template with {{ name }}
        'site_name': 'MyApp',
    })
    plain_body = f'Hello {user_name}, your registration is confirmed.'
    msg = EmailMultiAlternatives(
        subject='Registration Confirmed',
        body=plain_body,
        from_email='noreply@example.com',
        to=[user_email],
    )
    msg.attach_alternative(html_body, 'text/html')
    msg.send()

def send_notification(request):
    comment = request.POST.get('comment', '')
    # SECURE OPTION 2: Escape user input before including in HTML
    escaped_comment = escape(comment)
    html_body = f'<p>New comment: {escaped_comment}</p>'
    msg = EmailMessage(
        subject='New Comment',
        body=html_body,
        from_email='noreply@example.com',
        to=['admin@example.com'],
    )
    msg.content_subtype = 'html'
    msg.send()

```

## 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-060",
    name="Django XSS in HTML Email Body",
    severity="MEDIUM",
    category="django",
    cwe="CWE-79",
    tags="python,django,xss,email,html,OWASP-A03,CWE-79",
    message="User input in HTML email body. Sanitize content before sending.",
    owasp="A03:2021",
)
def detect_django_email_xss():
    """Detects user input flowing into EmailMessage body."""
    return flows(
        from_sources=_DJANGO_SOURCES,
        to_sinks=[
            calls("EmailMessage"),
            calls("django.core.mail.EmailMessage"),
        ],
        sanitized_by=[
            calls("escape"),
            calls("strip_tags"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Render HTML emails using Django template files which auto-escape all {{ variable }} interpolations by default.
- When constructing HTML email content in Python code, escape all user-controlled values with django.utils.html.escape() before inclusion.
- Use EmailMultiAlternatives to send both a plain text and HTML version; generate the plain text version from the escaped HTML using strip_tags().
- Validate email addresses with Django's EmailValidator before using them as recipients.
- Consider using a dedicated email templating service that enforces sanitization, or a library like premailer for CSS inlining that also performs escaping.

## Security Implications

- **Phishing via Email HTML Injection:** An attacker who controls any text that appears in an HTML email can inject
HTML to overlay fake content on top of the legitimate email body. Adding a
fake "Verify your password" form with an attacker-controlled action URL inside
a legitimate-looking company email is a highly effective phishing vector.

- **Malicious Link Injection:** HTML injection in emails allows inserting hyperlinks with deceptive display
text but attacker-controlled href attributes. Recipients who see a link labeled
"Reset your password here" may not check the actual URL, particularly on
mobile devices where URLs are hidden.

- **JavaScript Execution in Permissive Email Clients:** While major webmail services strip scripts, some corporate email servers,
Outlook desktop client configurations, and older email systems render HTML
with JavaScript. Organizations with mixed email client environments should
treat email HTML injection with the same severity as browser XSS.

- **Content Spoofing and Brand Impersonation:** An attacker who triggers an application to send an HTML email with injected
content can make legitimate company emails appear to contain fraudulent
information, damaging brand trust and potentially creating legal liability.


## FAQ

**Q: Is HTML injection in emails as serious as XSS in web responses?**

HTML injection in emails is less severe than browser XSS because email clients
generally strip JavaScript. However, it is still a medium-severity finding because:
(1) phishing via HTML injection is highly effective, (2) some email clients execute
JavaScript, and (3) HTML injection undermines user trust in application emails. It
should be fixed using the same escaping discipline as web XSS.


**Q: Does using Django's template engine for email bodies automatically prevent this?**

Yes. render_to_string() with a template file auto-escapes all {{ variable }}
interpolations, just like rendering web views with render(). The vulnerability
only arises when HTML email content is constructed with Python string operations
(f-strings, concatenation) that include unescaped user data.


**Q: Can an attacker inject into email recipients (the to/cc/bcc fields)?**

Email header injection is a separate vulnerability (CWE-93) where user input
in the To, CC, or Subject headers contains newline characters that inject
additional headers. Django's EmailMessage validates and sanitizes these fields,
but you should still validate email addresses with EmailValidator before using
them as recipients.


**Q: What about plain text email bodies? Do they need escaping?**

Plain text email bodies (content_subtype='plain', the default) do not render
HTML, so HTML injection is not applicable. However, user input in plain text
emails can still be used for social engineering. For plain text emails,
ensure that user input does not contain misleading content.


**Q: How do we handle rich text email content from a WYSIWYG editor?**

Process WYSIWYG editor output through bleach.clean() with a strict allowlist
of permitted tags and attributes before including it in HTML emails. This
permits formatting (bold, italic, links) while removing script tags, event
handlers, and other dangerous HTML. Always specify strip=True in bleach.clean()
to remove disallowed elements rather than escaping them.


## 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 EmailMessage documentation](https://docs.djangoproject.com/en/stable/topics/email/#emailmessage-objects)
- [Django email templating](https://docs.djangoproject.com/en/stable/topics/email/#sending-html-email)
- [OWASP HTML Injection](https://owasp.org/www-community/attacks/HTML_Injection)
- [Django Template Auto-escaping](https://docs.djangoproject.com/en/stable/ref/templates/language/#automatic-html-escaping)

---

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