What is XSS and how do you prevent it?
XSS (cross-site scripting) is when an attacker manages to get their own script into a page on your site. Because the script comes from your site's address, the visitor's browser trusts it as if it were your site's own code: it can change the content of the page, send requests on the user's behalf, display a fake login form or read an unprotected session cookie.
The cause of the vulnerability is that data coming from the user is written to the page without being escaped. That is also the heart of the solution: every piece of data is escaped in a way that suits the context it is written into. This guide explains the concept, its types and how to apply the defence correctly with PHP.
In brief
- XSS arises when user data is written to the page without being escaped.
- Protection happens at output: every piece of data is escaped for the context it is written into.
- In JavaScript, data is written with
textContent; rich text is sanitised with an allowlist. - CSP and HttpOnly cookies are a second layer that limits the damage when a mistake is made.
On this page
How does XSS come about?
-
Three types, one cause
- Stored XSS: The harmful content is saved in the database (a comment, a profile name, a support ticket) and shown to everyone who opens that page. This is the type with the widest impact; records displayed in the admin panel affect the administrator too.
- Reflected XSS: The harmful content comes from a parameter in the request and is written straight back in the response (a search term, an error message). The user has to click a specially crafted link.
- DOM-based XSS: The problem is not on the server but in the JavaScript code in the browser; the script takes data from the address or another source and writes it to the page as HTML.
The cause is the same in all three: untrusted data has been placed, unescaped, somewhere on the page where it can be interpreted as code.
: Enlarge -
Protection happens at output
Instead of "cleaning" data when you save it, escape it when you write it to the page. The same data may be used in HTML one day and in an email or a JSON response the next; each has a different escaping rule. Storing the raw data in the database and escaping it by context at output is both safe and consistent.
: Enlarge -
The context determines the escaping
The HTML body, an HTML attribute, the inside of JavaScript and a URL parameter are separate contexts, and each has its own rule. Escaping that is right for HTML is not enough inside JavaScript. In the section below you will find the right method for each context.
: Enlarge -
Second layers: CSP and cookie flags
If a spot is missed in the escaping, you need layers that will limit the damage: Content-Security-Policy tells the browser which sources it may run scripts from; the HttpOnly flag prevents the session cookie from being read by JavaScript.
: Enlarge
Signs: how do you spot it?
- In a code review: Output produced without escaping, such as
echo $_GET[...]or<?= $variable ?>; on the JavaScript side, user data being written to the page withinnerHTML,document.write, or jQuery's.html()and.append(). - On the site: If you type a harmless piece of text containing an HTML tag (for example a bold tag) into a field and it comes out formatted instead of appearing exactly as typed, that field is not being escaped. Only try this on your own site.
- Visitor complaints: Unexpected redirects, pop-up windows, content on the page that is not yours.
- CSP reports: If reporting is switched on, attempts to load scripts from sources that are not allowed are reported.
Escaping output by context
HTML body and attributes
In PHP, use the htmlspecialchars function with ENT_QUOTES and the character set. A short helper function ensures the same setting is used everywhere. Always put attribute values in quotes; in an unquoted attribute, escaping is not enough.
<?php
// A single helper: the same setting everywhere
function e($deger): string
{
return htmlspecialchars((string) $deger, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
?>
<p>Hello, <?= e($kullaniciAdi) ?></p>
<input type="text" name="q" value="<?= e($_GET['q'] ?? '') ?>">Passing data into JavaScript
When passing data from PHP to JavaScript, do not write the value between quotes by hand; pass it with json_encode and with the flags that convert HTML-sensitive characters. An even safer way is to put the data in a data- attribute and read it from JavaScript.
<?php
$veri = ['ad' => $kullaniciAdi, 'sepet' => $sepetAdedi];
$bayraklar = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE;
?>
<script>
var sayfaVerisi = <?= json_encode($veri, $bayraklar) ?>;
</script>
<!-- Safer option: carry the data in a data- attribute -->
<div id="kullanici" data-ad="<?= htmlspecialchars($kullaniciAdi, ENT_QUOTES, 'UTF-8') ?>"></div>URLs and links
Data written into the parameter value of a URL is encoded with rawurlencode. If an address supplied by the user is going to be written into an href or src, check its scheme in addition to escaping it: only allow addresses that start with http or https.
<?php
// Parameter value: rawurlencode
$adres = 'arama?q=' . rawurlencode($terim);
// Address supplied by the user: allow only the http/https schemes
function guvenliAdres(string $adres): string
{
$sema = strtolower((string) parse_url($adres, PHP_URL_SCHEME));
return in_array($sema, ['http', 'https'], true) ? $adres : '#';
}
?>
<a href="<?= htmlspecialchars($adres, ENT_QUOTES, 'UTF-8') ?>">Results</a>
<a href="<?= htmlspecialchars(guvenliAdres($profilSitesi), ENT_QUOTES, 'UTF-8') ?>" rel="noopener nofollow">Website</a>Places to avoid
Do not write untrusted data directly inside a <script> block, into event attributes (such as onclick), inside <style>, or as a tag or attribute name. Safe escaping is difficult in these contexts; carry the data in a data- attribute instead.
Safe use on the browser side (DOM)
When writing data to the page in JavaScript, use methods that write it as text rather than methods that interpret it as HTML. textContent always inserts the data as plain text.
// WRONG: the data is interpreted as HTML
// kutu.innerHTML = 'Hello ' + ad;
// RIGHT: the data is inserted as plain text
kutu.textContent = 'Hello ' + ad;
// When creating a new element
var satir = document.createElement('li');
satir.textContent = yorum.metin;
liste.appendChild(satir);Frameworks such as React and Vue escape values in templates by default. The risk returns with the features that deliberately switch this protection off (dangerouslySetInnerHTML, v-html). Only give these features sanitised HTML.
Rich text: sanitising with an allowlist
If the user needs to enter HTML in fields such as a blog editor or a product description, escaping cannot be used, because the formatting is meant to be visible. In this case the HTML is sanitised against an allowlist: only certain tags (such as paragraph, bold, list and link) and certain attributes are allowed, and everything else is discarded.
- Do not write your own regular expressions for sanitising; filtering HTML safely with regular expressions is not possible in practice. Use a maintained library built for the job (such as HTML Purifier in PHP or DOMPurify in the browser).
strip_tagsis not enough on its own: it does not clean the attributes of the tags you allow.- Do the sanitising on the server; do not trust the HTML produced by the editor in the browser.
- In links, only allow the
http,httpsandmailtoschemes.
Content-Security-Policy (CSP)
CSP is a response header that tells the browser which sources the page may load scripts, styles and images from. A policy that does not allow inline scripts prevents a script that has somehow got into the page from running. CSP does not replace escaping; it is the seat belt that comes into play when a mistake is made.
On an existing site, a strict policy can break pages. Start in report-only mode; nothing is blocked, and violations appear only in the browser console and, if you have defined one, at the reporting address. Move to enforcement once you have resolved the violations.
<IfModule mod_headers.c>
# Stage 1: report only, block nothing
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
# Stage 2: once the violations are resolved, change the name of
# the header to Content-Security-Policy
</IfModule>Moving inline scripts into separate files is the most important step in tightening CSP. For inline scripts that cannot be moved, a nonce value that changes on every request can be used. For details, see the Security headers and HTTPS guide.
Protect the session cookie
When the session cookie is marked HttpOnly, it cannot be read by JavaScript; even if there is an XSS vulnerability, the cookie cannot be stolen directly. The Secure flag ensures the cookie is sent only over HTTPS, and SameSite restricts it from being sent with requests coming from other sites.
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true, // HTTPS only
'httponly' => true, // JavaScript cannot read it
'samesite' => 'Lax',
]);
session_start();HttpOnly does not prevent the other effects of XSS (performing actions on the user's behalf, changing the page content); the real solution is still escaping.
Checklist
- All variable output in templates goes through the escaping helper.
- Attribute values are in quotes.
- Data is passed to JavaScript with
json_encodeor adata-attribute. - Addresses supplied by users have their scheme checked.
- In JavaScript, user data is written with
textContent; uses ofinnerHTMLhave been reviewed. - Rich text is sanitised on the server with an allowlist-based library.
- CSP is defined, at least in report-only mode.
- The session cookie is issued with the HttpOnly, Secure and SameSite flags.
- User input listed in the admin panel (form submissions, comments) is escaped as well.
Frequently asked questions
If I clean the input when saving it, do I still need to escape at output?
Yes. Protection happens at output, because the same data can be used in different contexts and data can also enter the database by other routes. At input, only validate the format; leave escaping to the output.
Does using strip_tags prevent XSS?
It is not reliable on its own. It is no use in an attribute context and does not clean the attributes of allowed tags. Use htmlspecialchars for plain-text fields and an allowlist-based sanitising library for rich text.
If I add CSP, is escaping still necessary?
Yes, it is. CSP is the second line of defence; it can be bypassed through old browsers, misconfiguration or the sources it allows. The real protection is correct escaping.
Is escaping also needed on pages that only administrators see?
Yes, especially there. Fields that anyone can fill in, such as a contact form or comments, are listed in the admin panel; if they are not escaped, the script runs in the administrator's session.
BYK Yazılım Support Team
This guide is written and regularly reviewed by the BYK Yazılım support team. Last updated: 4 October 2026.
Related guides
- What is CSRF and how do you prevent it?CSRF tokens, SameSite cookies, Origin checks and using POST for state-changing actions.
- Security headers and HTTPSHSTS, CSP, X-Content-Type-Options, Referrer-Policy and Permissions-Policy, with an .htaccess example.
- Login and session securityPassword hashing, attempt limits, session fixation, cookie flags and authorisation checks on every request.
- What is SQL injection and how do you prevent it?Prepared statements, allowlists, a least-privilege database user and hiding error messages.
Let us review your website together
BYK Yazılım builds corporate websites. Write to us with any questions about your site.
Contact us Our corporate website service