Cross-site scripting (XSS) lets an attacker run their JavaScript in your visitors' browsers, on your domain, with your users' sessions. It is one of the most common web application flaws, and OWASP groups it under Injection in its Top 10. This guide covers the three kinds of XSS, the output-encoding rules that stop them in PHP and Node.js, and how a Content Security Policy catches what slips through.
Stop XSS by encoding every piece of untrusted data for the place it's output: HTML text, an attribute, a URL or JavaScript. Use your template engine's escaping form ({{ }} in Blade and Twig, <%= %> in EJS, plain JSX in React) and audit every raw-output escape hatch. Sanitise user HTML with an allow-list library such as DOMPurify or HTML Purifier. Then add a Content Security Policy as a second line of defence.
1. What XSS is and what it can do
XSS happens when user-supplied content reaches a page's HTML, attributes or scripts without being encoded for that context, so the browser runs it as code. One working XSS on a site with logins can:
- act as the logged-in user: change their email, place orders, delete content;
- read anything the user can see, including private messages and admin screens;
- steal session cookies, unless they are marked
HttpOnly; - redirect visitors to phishing pages or malware.
XSS and SQL injection share a root cause, mixing code with data, but they need different defences at different layers. For the database side, see Preventing SQL injection in PHP and Node.js.
2. The three types
| Type | Where the payload lives | Typical source | Why it's dangerous |
|---|---|---|---|
| Stored | In your database | Comments, profiles, reviews, support messages | Every visitor to the page runs it |
| Reflected | In the request (URL or form) | Search boxes, error messages | The attacker sends victims a crafted link |
| DOM-based | Only in the browser | JavaScript reading location.hash or postMessage data | The server never sees the payload |
A reflected example in PHP:
echo "Search results for: " . $_GET['q']; // vulnerableA DOM-based example in JavaScript:
document.getElementById('greeting').innerHTML = 'Hello ' + location.hash.slice(1); // vulnerableThe fix for the DOM case is to use textContent instead of innerHTML whenever you are inserting text.
3. Defence 1: encode output for its context
This is the primary defence. Each output context needs its own encoding:
| Output context | What to do |
|---|---|
| HTML body text | HTML-escape &, <, >, " and ' |
| HTML attribute value | HTML-escape and always quote the attribute |
URL in href or src | Allow only http: and https: schemes, then encode |
| Inside a script block | Pass data as JSON with HTML-safe flags, never by string concatenation |
| Inside CSS | Avoid user data; if unavoidable, allow-list the value (a colour, a number) |
PHP
function e(string $v): string {
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}
echo '<a href="' . e($safeUrl) . '">' . e($userName) . '</a>';Since PHP 8.1, htmlspecialchars encodes both single and double quotes by default, but passing the flags and 'UTF-8' explicitly keeps the code safe and clear on any version. Template engines do this for you:
- Blade (Laravel):
{{ $var }}escapes;{!! $var !!}does not. Audit every{!! !!}. - Twig:
{{ var }}escapes when autoescape is on (the default);|rawdoes not. - WordPress:
esc_html()for text,esc_attr()for attributes,esc_url()for links,wp_kses()to allow a limited set of tags.
Node.js
- React and Next.js: JSX escapes values automatically. The escape hatch is
dangerouslySetInnerHTML; pass it only sanitised HTML. Also validate URLs you put inhref, because ajavascript:URL is not fixed by escaping. - Vue:
{{ }}andv-textare safe;v-htmlis not. - EJS:
<%= %>escapes;<%- %>does not. - Handlebars:
{{ }}escapes;{{{ }}}does not. - Pug:
#{}escapes;!{}does not.
Every mainstream engine is safe by default with an explicit opt-out. Search your code base for the opt-out forms: that is where XSS lives. Avoid building HTML with string concatenation; if you must, escape with a small, tested helper and keep it in one place.
4. Sanitise HTML you have to accept
Rich-text fields (blog posts, formatted comments, WYSIWYG editors such as TinyMCE or CKEditor) produce HTML, which escaping would destroy. Clean it with an allow-list sanitiser instead:
- PHP: HTML Purifier.
- JavaScript: DOMPurify (in the browser, or on the server with a DOM implementation such as jsdom), or
sanitize-html.
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userHtml);Sanitise on the server before storing, and treat anything from the editor as untrusted. If you accept Markdown, sanitise the HTML the Markdown library produces: current Markdown libraries don't sanitise by themselves.
5. Defence 2: a Content Security Policy
A Content Security Policy (CSP) tells the browser which scripts may run. With a strict policy, an injected script tag or inline event handler is blocked even if an escape was missed.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-RANDOM'; object-src 'none'; base-uri 'self'Generate a new random nonce for every response and add it as a nonce attribute on each legitimate script tag. Avoid 'unsafe-inline' in script-src, which cancels most of the protection. Deploy it first as Content-Security-Policy-Report-Only, fix what it reports, then enforce it. For DOM XSS, modern browsers also support Trusted Types (require-trusted-types-for 'script'), which blocks raw string assignments to sinks such as innerHTML.
The old X-XSS-Protection header is obsolete; leave it out or set it to 0. The full header set, with Apache, nginx and Node.js examples, is in Security headers explained: CSP and HSTS.
6. Supporting layers
- Input validation. Reject what can't be valid early: a username of letters, digits and underscores; an email that parses as an email; an integer in range. Use Laravel's validator or a schema library such as zod. It reduces risk but never replaces output encoding.
- Cookies. Set session cookies
HttpOnly,SecureandSameSite=Lax(orStrict).HttpOnlystops scripts reading the cookie; XSS can still act as the user, so it limits the damage rather than preventing it. - No
eval. Never pass user input toeval(),new Function()orsetTimeoutwith a string. UseJSON.parse()for data.
Escaping when saving to the database instead of when outputting. Double-escaping, so users see &lt;. Using strip_tags as XSS protection: it doesn't make attribute or URL contexts safe. Validating URLs with filter_var(FILTER_VALIDATE_URL) alone, which accepts javascript: URLs; check the scheme too. Printing JSON into a script block without JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT. Blocking the word "script" while event handlers like onerror still get through.
7. Test for XSS
On a staging copy of your own site, enter harmless payloads like these into every field, URL parameter and header you reflect:
"><img src=x onerror=alert(1)>
<svg onload=alert(1)>
javascript:alert(1)If an alert box appears, you have XSS; if the text shows literally, encoding is working.
Tools that help:
- Dynamic scanners: OWASP ZAP (free) and Burp Suite Professional. Burp's free Community edition has no automated scanner, but is useful for manual testing.
- Static analysis: Psalm's taint analysis for PHP, Semgrep rules for PHP and JavaScript, and
eslint-plugin-securityfor Node.js.
Test only sites you own or have written permission to test.
8. Running this on Domain India
Our cPanel servers run Imunify360 with its full ModSecurity ruleset, measured in September 2026, which blocks many common attack requests before they reach PHP. It may also block your own test payloads, so a test that "passes" on the live site can hide a real bug; test on staging and in code review too. A firewall is a safety net for known patterns, not a replacement for encoding in your code.
- Security headers: on cPanel and DirectAdmin hosting, set CSP and the other headers in
.htaccesswithHeader always set;mod_headersis loaded on both. - Node.js apps: run on shared cPanel hosting through Setup Node.js App, or on the App Platform; the same encoding rules apply.
- If a site is already compromised: follow the security checklist for a hacked or defaced website.
For the wider picture, read OWASP Top 10: practical defence for PHP and Node.js and CSRF tokens deep dive.
What is the most important defence against XSS?
Encoding output for its context: HTML-escape text and attributes, allow only http and https in URLs, and pass data into scripts as safely encoded JSON. Template engines do this by default, so the main job is auditing every place where escaping is switched off.
Can XSS steal a logged-in user's session?
It can read session cookies that are not marked HttpOnly. With HttpOnly set, scripts can't read the cookie, but an XSS payload can still send requests as the user from their browser, so HttpOnly limits the damage without preventing it.
Is React safe from XSS by default?
Mostly. JSX escapes values automatically. The risks are dangerouslySetInnerHTML with unsanitised HTML, and user-supplied URLs in href or src that use the javascript: scheme, so sanitise the first and validate the second.
Does a Content Security Policy replace output encoding?
No. CSP is a second line of defence that blocks many injected scripts when an escape is missed. Encode output first, and use a nonce-based CSP without unsafe-inline as backup.
Should I use strip_tags to prevent XSS in PHP?
No. strip_tags removes tags but doesn't make attribute or URL contexts safe. Use htmlspecialchars with ENT_QUOTES and UTF-8 for output, and HTML Purifier when you must accept user HTML.
How do I safely display user-written HTML such as blog posts?
Sanitise it with an allow-list sanitiser such as DOMPurify or HTML Purifier before storing it, and output only the sanitised version. If you accept Markdown, sanitise the HTML that the Markdown library produces.
Ready to harden your site? Add the headers from Security headers explained, then review your templates for raw-output forms. To host PHP or WordPress sites, compare cPanel hosting; for Node.js apps, see the App Platform.
Imunify360 and ModSecurity run on our cPanel servers, alongside free SSL and weekly backups.
See cPanel plans