Skip to main content
Mohammad Shehadeh — home (MSH monogram, letter M filled with the Palestinian flag)

XSS Attacks: How Malicious Code Hijacks Web Applications

Published on
4 min read

An attacker posts a comment on a social media site with hidden JavaScript inside it. When other users view the comment, the code runs in their browsers and steals their login tokens and personal data. This is XSS—Cross-Site Scripting—one of the most common web vulnerabilities.

Unlike CSRF, which tricks browsers into making requests, XSS injects malicious code straight into the page. The attacker's script runs with the same permissions as legitimate code, so it can reach session data, cookies, and sensitive information.

How XSS Attacks Work

XSS exploits the trust between users and websites. When a site renders user-supplied data without validation, attackers inject JavaScript that runs in visitors' browsers.

For example, a website displays user comments without filtering:

vulnerable-comment.html
1<!-- User enters this as a comment -->
2<img src=x onerror="fetch('/steal-session?token=' + document.cookie)">
3
4<!-- Website renders it as-is -->
5<div class="comment">
6  <img src=x onerror="fetch('/steal-session?token=' + document.cookie)">
7</div>
8
9<!-- Script executes when image fails to load -->

When the page displays this comment, the image fails to load and triggers the onerror event. The JavaScript runs in the visitor's browser with full access to their data.

Critical Difference

Unlike CSRF, XSS doesn't require the user to visit another site. The attacker's code runs on the legitimate website itself, with full access to page content, cookies, and session data.

Types of XSS Attacks

Stored XSS

The malicious code is saved in the database and served to every user who views it:

stored-xss-flow.js
1// Attacker submits malicious comment
2const comment = "<script>stealUserData()</script>";
3database.saveComment(comment); // Stored in database
4
5// When other users load the page
6const displayedComment = database.getComment(); // Dangerous!
7document.innerHTML = displayedComment; // Script executes for everyone

This is the most dangerous type because it hits every user who views the malicious content.

Reflected XSS

The malicious code is sent through a URL parameter and reflected back in the response:

reflected-xss-example.js
1// Attacker sends this link to victims
2// https://example.com/search?q=<script>stealCookies()</script>
3
4// Server reflects the query parameter without encoding
5app.get('/search', (req, res) => {
6  const query = req.query.q; // Attacker's script
7  res.send(`<h1>Search results for: ${query}</h1>`); // Vulnerable!
8});
9
10// Victim clicks link → script executes in their browser

Reflected XSS needs the victim to click a malicious link, so it's less damaging than stored XSS.

DOM-Based XSS

JavaScript updates the DOM with user input without validation:

dom-xss.js
1// Vulnerable client-side code
2const userInput = document.getElementById('search').value;
3document.getElementById('results').innerHTML = userInput; // Dangerous!
4
5// If user enters: <img src=x onerror="alert('XSS')">
6// The script executes immediately

DOM-based XSS happens entirely on the client and is harder to detect.

Why XSS is Dangerous

XSS gives attackers access to:

  • Session cookies and authentication tokens
  • Personal data on the page
  • Users' browsing history
  • Ability to perform actions as the user
  • Malware distribution
  • Phishing attacks

A single XSS vulnerability can compromise thousands of users.

Defense Strategies

Input Validation

Validate all user input and reject anything that doesn't match the expected format. Sanitize dangerous content before storing it:

input-validation.ts
1import DOMPurify from 'dompurify';
2
3// Sanitize HTML by removing dangerous tags and attributes
4const cleanComment = DOMPurify.sanitize(userInput);
5database.saveComment(cleanComment);
6
7// Or validate expected format strictly
8const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9if (!emailRegex.test(userEmail)) {
10  throw new Error('Invalid email');
11}
Best Practice

Never trust user input. Validate format first, then sanitize (remove dangerous content) or encode (escape special characters) before storing or displaying.

Output Encoding

When you display data, encode it by converting special characters to HTML entities. This differs from sanitization—encoding keeps the data but makes it safe:

output-encoding.js
1// Dangerous - renders HTML and scripts
2document.innerHTML = userComment; // ❌
3
4// Safe - renders as plain text only
5document.textContent = userComment; // ✅
6
7// Or encode HTML entities (convert < > & etc to safe versions)
8const encoded = userComment
9  .replace(/&/g, '&amp;')
10  .replace(/</g, '&lt;')
11  .replace(/>/g, '&gt;')
12  .replace(/"/g, '&quot;')
13  .replace(/'/g, '&#x27;');
14document.innerHTML = encoded; // ✅

Content Security Policy (CSP)

Set a CSP header to control where scripts can load from:

csp-header.js
1// Server header
2res.setHeader('Content-Security-Policy',
3  "default-src 'self'; script-src 'self' https://trusted-cdn.com"
4);
5
6// This blocks inline scripts and scripts from untrusted sources
7// Attacker's injected code: <script>alert('XSS')</script> ❌ Blocked

CSP is powerful but needs careful configuration to avoid breaking legitimate functionality.

HttpOnly Cookies

Prevent JavaScript from accessing session cookies:

httponly-cookie.js
1res.cookie('sessionId', token, {
2  httpOnly: true,  // JavaScript cannot access this cookie
3  secure: true,    // HTTPS only
4  sameSite: 'Lax'  // CSRF protection
5});
6
7// Attacker's code cannot steal the cookie
8// fetch('/steal?cookie=' + document.cookie) // Won't work

Quick Defense Checklist

  • ✅ Validate input format before processing
  • ✅ Sanitize user input (remove dangerous content)
  • ✅ Encode output when displaying user data (escape special characters)
  • ✅ Use frameworks with built-in XSS protection (React handles encoding automatically)
  • ✅ Set Content Security Policy headers
  • ✅ Mark sensitive cookies as httpOnly
  • ✅ Use security libraries like DOMPurify for HTML sanitization
  • ✅ Keep dependencies updated for security patches
  • ✅ Test for XSS vulnerabilities regularly

Real-World Examples

XSS has compromised major platforms:

  • Facebook (2013): Attackers embedded code in comments
  • Twitter (2014): DOM-based XSS allowed account hijacking
  • Google (2019): Multiple XSS vulnerabilities found in services

These incidents led to stricter input validation and CSP adoption across the industry.

Conclusion

XSS attacks are common because they're simple to run but easy to overlook. A single validation or encoding mistake can expose thousands of users to attack.

The defense takes three layers: validate input format, sanitize dangerous content, and encode output. Modern frameworks like React encode automatically, but custom DOM manipulation needs care.

Never assume user input is safe. Always validate format, sanitize content, encode output, and use CSP.

Related Articles

GET IN TOUCH

Let's work together

I build fast, accessible, and delightful digital experiences for the web. Whether you have a project in mind or just want to connect, I'd love to hear from you.

Get in touch

or reach out directly at hello@mohammadshehadeh.com