Cross-site scripting is one of the oldest and most common security problems on the web. Attackers find ways to inject malicious scripts into your pages, and when those scripts run in your users browsers, bad things happen. Session cookies get stolen. Passwords get captured. Users get redirected to fake sites. Your reputation takes a hit.
The good news is that defending against XSS is well understood. There are proven techniques that work. Some of them are built right into browsers. Others require you to change how you build your sites. Used together, they create a strong defense that stops most XSS attacks before they cause harm.
Imagine you have a comment section on your blog. A user posts a comment that contains JavaScript code instead of plain text. For example, they might try to inject something like script alert hacked script. If you do not filter or escape that input properly, that code becomes part of your page. Every visitor who views that comment sees the popup. That is a simple example. A real attack might send cookies to a remote server, redirect users to a phishing site, or deface your homepage.
There are three main types of XSS. Reflected XSS happens when a script is embedded in a URL. Someone clicks a link that looks legitimate but includes malicious code. The site reflects that code back in the response. The browser runs it. Stored XSS is when the script is saved in your database and served to anyone who views that content. This is the most dangerous because it can affect many users. DOM-based XSS happens entirely in the browser, when client-side JavaScript modifies the page in unsafe ways.
Each type requires different defense strategies, but the underlying problem is the same. Untrusted data is being treated as executable code. The solution is to make sure that does not happen.
Most modern browsers have built-in XSS filters. Chrome calls it XSS Auditor. Firefox has a similar feature. These filters look at incoming requests and outgoing responses. If they detect a script in the URL that appears in the response, they block it. The idea is to catch reflected XSS attacks before they execute.
The X-XSS-Protection header gives you control over how these filters behave. You can turn them on, off, or set them to block mode. The header values are simple. X-XSS-Protection: 0 disables the filter. X-XSS-Protection: 1 enables it in sanitization mode. The filter tries to remove the dangerous parts and keep the rest of the page working. X-XSS-Protection: 1; block tells the browser to block the whole page if an attack is detected.
The block mode is the strongest option. If an attack is detected, the browser shows an error page instead of trying to fix the page. This prevents the attack from executing, and it also prevents any broken rendering that might happen when the filter removes parts of your page. The error page is clear. Something bad was attempted, and the browser stopped it.
Here is the header you should use:
X-XSS-Protection: 1; block
Browser XSS filters are helpful, but they are not a complete solution. They mainly target reflected XSS. Stored XSS attacks often go unnoticed because the script does not appear in the URL. DOM-based XSS can also bypass the filter because the injection happens in client-side code after the page has loaded.
Another limitation is that not all browsers have XSS filters, and some users disable them. Firefox had its XSS filter turned off by default for a while. Some security tools recommend disabling the filter for performance reasons. You cannot rely on the browser to protect your users. You need your own defenses.
There is also a quirk in how the header works. Some older browsers treat the header differently. The block directive is not supported everywhere. But using the header does not hurt. It gives you protection where it works and does nothing where it does not.
The most important thing you can do to prevent XSS is to encode output correctly. When you put user data into HTML, you need to escape it so that it becomes plain text instead of code. The exact escaping depends on where the data goes.
If you are putting data into HTML body, you escape HTML characters. Less than becomes ampersand l t semicolon. Greater than becomes ampersand g t semicolon. Ampersand becomes ampersand a m p semicolon. Quotes become ampersand q u o t semicolon. This makes sure that any HTML tags or script tags show up as text instead of being interpreted.
If you are putting data into an attribute, you need a different kind of escaping. If you are putting data into JavaScript, you need yet another. The context matters. Using HTML escaping on data that goes into a JavaScript string does not work. An attacker can still break out of the string and execute code.
Most web frameworks have built-in escaping that works by default. They escape output automatically unless you tell them not to. This is good. You should rely on this feature. The problems happen when developers turn off escaping because they want to allow HTML. That is where XSS vulnerabilities come from.
If you need to allow some HTML, use a proper sanitizer. Sanitizers parse the HTML, remove dangerous tags and attributes, and output safe HTML. This is harder than escaping, but it can be done safely with well-tested libraries. Do not try to write your own sanitizer. You will miss something.
We talked about CSP in another article, but it is worth mentioning here because it is one of the best defenses against XSS. CSP tells the browser exactly what scripts are allowed to run. If an attacker injects a script, the browser checks the CSP. If that script does not come from an approved source, it does not run.
CSP protects against all types of XSS. Reflected, stored, DOM-based. It does not matter. If the script is not from an allowed domain, or if it is inline without a proper nonce or hash, it gets blocked. This is powerful because it works even if your output encoding fails.
A strict CSP with script-src 'self' and no 'unsafe-inline' stops most XSS attacks cold. Inline scripts do not run. Scripts from unknown domains do not run. Attackers have no way to execute their code. Your site becomes much harder to exploit.
Even if an attacker finds a way to run scripts on your page, you can limit the damage. One way is to protect your cookies. Session cookies that are not marked HttpOnly can be read by JavaScript. An XSS attack can grab those cookies and send them to the attacker. The attacker then uses those cookies to impersonate the user.
Setting the HttpOnly flag on your cookies stops this. HttpOnly cookies cannot be accessed by JavaScript. The browser sends them automatically with requests, but the page script cannot read them. This means an XSS attack cannot steal session cookies. The attack might still do other things, but it cannot take over the user's session.
The Secure flag is also important. It tells the browser to only send the cookie over HTTPS. This prevents the cookie from being exposed on insecure connections. Combined with HttpOnly, your cookies become much harder to steal.
Here is how a secure session cookie header looks:
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax
Validating user input is another important layer. When you know what data should look like, you can reject anything that does not match. If a field expects a phone number, validate that it contains only digits. If it expects an email, validate the format. If it expects a username, reject any characters that are not allowed.
Input validation does not replace output encoding. An attacker might still find a way to bypass validation. But it makes their job harder. It also catches many accidental injections that might happen through normal use.
Validate on the server. Client-side validation is for user experience, not security. An attacker can bypass any client-side checks. All validation that matters for security must happen on the server where you control the code.
Knowing how attackers think helps you defend better. A common pattern is injecting script tags. If you allow user input anywhere that becomes HTML, an attacker will try using script tags with alert calls. If you block that, they try using image tags with onerror handlers. This uses an image tag that fails to load and runs JavaScript in the onerror handler.
Another pattern is using javascript colon URLs. If you put user data into a link href, an attacker might use a javascript scheme with an alert. The browser runs the script when the link is clicked. If you put data into an iframe src, the same attack works.
Event handlers are another vector. onmouseover, onclick, onload. If you allow user data into any attribute that can contain JavaScript, attackers will try to inject event handlers. The best defense is to never put untrusted data into attributes that can execute code.
Modern frameworks take XSS seriously. React escapes everything by default. If you try to insert user data directly, React turns it into text, not HTML. You have to use a special property to bypass this, and the name is meant to warn you. Angular does similar escaping. Vue escapes by default as well.
If you are using a framework, rely on its built-in protections. Do not bypass them unless you absolutely have to. And if you do bypass them, use a sanitizer on the data first. The framework gives you a safe baseline. Your job is to not introduce unsafe patterns.
For server-side frameworks, check their escaping defaults. Many have auto-escaping templates. Use them. If your framework has a security guide, read it. They usually cover XSS prevention in detail with examples specific to that framework.
You cannot protect against XSS if you do not know where you are vulnerable. Testing should be part of your development process. Automated scanners can find many XSS issues. They send common payloads to your input fields and check if they get reflected. These tools are good at finding low-hanging fruit.
Manual testing catches more subtle issues. Think about every place user data appears on your site. In HTML body. In attributes. In JavaScript strings. In CSS. In URLs. Each context is different. Test each one. Try common payloads. See what happens.
If you have a bug bounty program or do penetration testing, XSS is usually high on the list. Attackers love finding XSS because it is often easy to exploit and can lead to serious consequences. Pay attention to what they find and fix it quickly.
To understand why XSS matters so much, consider what an attacker can do after injecting a script. They can steal session cookies and log in as the user. They can capture keystrokes and steal passwords as they are typed. They can make requests on behalf of the user to change account settings, make purchases, or delete data. They can deface your site and damage your reputation. They can redirect users to a fake version of your site that looks exactly like yours.
All of this happens inside the user's browser. The user has no idea anything is wrong. They trust your site. That trust gets exploited. The damage can be extensive. For e-commerce sites, XSS can lead to financial loss. For sites with personal data, it can lead to identity theft. For any site, it can lead to loss of user trust that takes years to rebuild.
No single defense is enough. Use multiple layers. Start with output encoding. Always escape user data when it goes into HTML. Use your framework's escaping features. Add a strict CSP that blocks inline scripts and scripts from unknown sources. Set X-XSS-Protection: 1; block to use the browser filters. Make your cookies HttpOnly and Secure so they cannot be stolen. Validate input to reject malformed data before it gets processed.
Each layer makes the attacker's job harder. They have to bypass multiple defenses to succeed. Most attacks are not sophisticated. They look for easy targets. If you have these basic protections in place, attackers will move on to someone else.
XSS has been around for decades because it is easy to do wrong and hard to do right. But the defenses are known. They are available. They work. Implement them, test them, and keep them updated. Your users will thank you.