Cross‑Site Scripting — or XSS — is one of the oldest yet most persistent web vulnerabilities. Despite decades of developer education and tooling improvements, XSS remains a frequent pitfall, especially in modern JavaScript‑heavy stacks like MERN (MongoDB, Express, React, Node).
Think of XSS like hidden graffiti — a malicious script snuck into your page that only reveals itself when an unsuspecting visitor arrives. Once executed, it behaves like part of your own code, running in the same security context. That's what makes it so dangerous.
Types of XSS: Reflected vs. Stored
Broadly speaking, there are three major kinds of XSS vectors, but the two most relevant for most web apps are reflected and stored:
🌀 Reflected XSS
Reflected XSS happens when user input is immediately embedded into a page's HTML response without proper sanitization or encoding. Attackers weaponize this by embedding malicious scripts in URLs or form parameters — then trick users into visiting them.
A simple example might be a search page that echoes the search term back into the page as HTML.
📌 Stored XSS
Stored XSS, also called persistent XSS, occurs when the malicious script is stored on the server — in a database, comment, profile field, or post — then delivered to every visitor who views that content. Because it's served directly from the backend, stored XSS is usually more dangerous than reflected XSS.
Both are highly relevant to modern full‑stack apps — especially when your React frontend displays user‑generated content without context‑aware sanitization.
PortSwigger Labs: Visualizing the Difference (Reflected vs Stored)
Security labs like PortSwigger provide interactive environments where you can exercise XSS payloads safely. The key takeaway from these labs:
- Reflected XSS requires crafting a URL that reflects malicious input back into the page.
- Stored XSS stores the malicious script on the server and executes it whenever the victim views that content.
For example, in Burp Suite, you might intercept a query parameter and send it to Repeater, then manipulate it to include <script>alert(1)</script> and observe how it executes in the browser — that's reflected XSS. These exercises illustrate why unescaped output leads to client‑side compromise.
Where MERN Apps Are Most at Risk
In a typical MERN stack application:
- Express APIs take user input (e.g., form submissions, messages, profile data) and save it in MongoDB.
- React fetches this data and renders it in the DOM.
If React components use dangerous methods like dangerouslySetInnerHTML or innerHTML, any script tags in the stored content will execute in the client browser. That opens the door to stored XSS — very much like leaving a blank wall for graffiti. Without output encoding and sanitization, any attacker can deface your UI.
A Real Example: Before and After Adding CSP
Here's a simple React page that renders user input and how it can be exploited:
🔥 Vulnerable Version (Before)
function Comment({ text }) {
return (
<div className="comment" dangerouslySetInnerHTML={{ __html: text }}></div>
);
}
// If `text` is: `<script>alert('XSS');</script>`This will execute the script every time a comment loads — classic XSS. In this scenario, the browser executes the injected script because it's indistinguishable from safe HTML.
🛡️ Secure Version (After): CSP + Safe Rendering
First, add a Content‑Security‑Policy header — either via your Express backend or via your hosting config:
// Express example
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; object-src 'none';"
);
next();
});Then, in your React component:
function Comment({ text }) {
return <div className="comment">{text}</div>; // avoids innerHTML
}With CSP, even if a malicious script somehow reaches the browser, the policy restricts execution to trusted origins. This dramatically reduces the impact of any XSS flaw that remains in your application.
Modern Mitigation Techniques
✅ Output Encoding
Always encode user‑controlled data before rendering. This converts dangerous characters (<, >, etc.) into safe entities (<, >). Most modern templating engines have built‑in output encoding.
✅ Avoid Dangerous APIs
React's dangerouslySetInnerHTML and direct DOM manipulation (like innerHTML) are major XSS vectors unless strictly sanitized. Whenever possible, use safe text‑rendering approaches (textContent, React's default escaping).
✅ Context‑Aware Sanitization
If you must render HTML user input, use trusted sanitization libraries that understand HTML contexts and escape appropriately.
✅ Content Security Policy (CSP)
CSP tells browsers which sources of JavaScript are allowed — acting like a whitelist for script execution. It's a powerful second layer of defense that significantly limits XSS even if a flaw exists.
Conclusion: Treat XSS Like Graffiti Prevention
Just as graffiti protects your physical walls, proper input/output handling and CSP protect your application's client side from defacement. Injecting scripts into a web page isn't just a nuisance — it's a real security risk that can leak data, hijack sessions, or undermine trust in your platform.
React and other modern frameworks help, but only when used with secure defaults and proper content policies. Take time to audit all places where user input flows into your UI — because once malicious script execution happens, the attacker's code runs just like yours.