In short: You prevent XSS in PHP by encoding every output to suit its context: htmlspecialchars() for HTML, json_encode() with protective options for JavaScript, vetted URLs for links. Templates with automatic escaping take much of this off your hands. A Content Security Policy and HttpOnly cookies limit the damage if a vulnerability does remain.
Why cross-site scripting should be checked now too
In October 2026 attention is on PHP2Shell, two critical vulnerabilities in the PHP interpreter for which patches have been available since 21 October 2026. Many companies are having their web applications reviewed as a result. That is a good occasion to also look at weaknesses in your own code that no PHP update fixes.
Cross-site scripting, XSS for short, is one of them. The OWASP Top 10 lists XSS within the Injection category. Unlike a vulnerability in the server, the attack here targets the visitors and users of your website, for example customers in the shop or staff in the backend.
The consequences can be considerable: hijacked administrator sessions, manipulated forms that send data to third parties, or forged content on your website. If an administrator account is taken over, this can go as far as complete control of the application.
How does XSS arise in PHP applications?
XSS arises when an application outputs data from an untrusted source into a page unchecked. The browser can then no longer distinguish between your content and planted code, and executes both.
Three forms are distinguished. With reflected XSS, the content comes directly from the current request, for example from a search term in the URL. With stored XSS, it sits in the database, for instance in a comment or profile field, and hits everyone who opens the page. DOM-based XSS arises in the browser's JavaScript when scripts write values from the URL into the page.
Untrusted is all data that is not fixed in the code: form fields, URL parameters, cookies, file names, responses from external interfaces and content from your own database that came from outside at some point.
Encoding output correctly for each context
The most important rule is: encode on output, and to suit the place where the value ends up. For HTML content and quoted attributes, htmlspecialchars() is the means of choice. A small helper function prevents options from being forgotten:
<?php
function e(?string $wert): string
{
return htmlspecialchars($wert ?? '', ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}
?>
<h2><?= e($produkt['name']) ?></h2>
<input type="text" name="suche" value="<?= e($_GET['q'] ?? '') ?>">
Other contexts need other means:
| Context | Safe method |
|---|---|
| HTML content, quoted attribute | htmlspecialchars() with ENT_QUOTES and UTF-8 |
| Data for JavaScript | json_encode() with JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_HEX_QUOT |
| Parameter in a URL | rawurlencode() for the individual value |
| Complete URL from user input | Check the scheme, allow only https and http, then htmlspecialchars() |
| Unquoted attributes, event handlers, CSS | Avoid; do not insert values there |
For data that a script needs, a data attribute or a JSON block is usually the cleanest route:
<?php
$daten = json_encode($konfiguration, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<div id="app" data-konfig="<?= e($daten) ?>"></div>
In JavaScript you then read the value via the data attribute and set content with textContent instead of innerHTML.
Templates, HTML sanitising and frameworks
Template engines such as Twig and Blade encode output automatically by default. That rules out a large part of typical mistakes. It becomes dangerous where this behaviour is switched off deliberately, for example with the raw filter in Twig or the {!! !!} syntax in Blade. Such places should be rare in the code and be checked specifically in every review.
If your application is meant to allow HTML from users deliberately, for example formatting in comments or in an editorial system, encoding is not enough, because the HTML is supposed to be displayed. You then need a vetted sanitising library such as HTML Purifier, which lets through only a fixed list of permitted elements and attributes. Custom filters with regular expressions have proved to be full of gaps.
Check backend areas in particular. XSS in views that only administrators see is often underestimated, but hits exactly the accounts with the most permissions.
Finding XSS in existing PHP code
In grown applications, the first question is usually not how to encode correctly, but where it has been forgotten so far. A pragmatic start is searching for places where variables are output directly:
# Direct output of request data
grep -rnE "(echo|print|<\?=)\s*\\\$_(GET|POST|REQUEST|COOKIE|SERVER)" src/ templates/ --include="*.php"
# Auto-escaping switched off in Twig and Blade
grep -rnE "\|raw|\{!!" templates/ resources/views/
Hits are items to investigate, not finished findings. A search like this finds stored XSS only to a limited extent, because the values there come from the database. Therefore also check all views that display content from users: comments, profiles, order notes, support tickets. With CMS and shops, third-party plugins and themes are explicitly included, because a large part of the output logic sits there.
Second line of defence: CSP and secure cookies
Even with careful code, a spot can be overlooked. Two measures then limit the damage. A Content Security Policy allows the browser only scripts from defined sources and forbids embedded scripts unless they are explicitly approved:
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
Introduce such a policy step by step, initially in report-only mode via Content-Security-Policy-Report-Only, because existing integrations of analytics or payment services usually have to be added. Further headers are described in the guide Security headers for websites.
In addition, session cookies should carry the HttpOnly attribute so that scripts cannot read them. How to secure sessions as a whole is in the guide Securing sessions in PHP. A related weakness on the database side is covered in Preventing SQL injection in PHP.
How does PHP SOS help?
- We check your application's templates and output for missing or incorrect encoding.
- We assess findings by reachability and affected accounts and show concrete fixes.
- We introduce a Content Security Policy step by step, without disrupting shop or payment functions.
- We test again after the fix and document the result.
An independent review is described in the service Security audit and penetration test.
Frequently asked questions
Is htmlspecialchars enough against XSS?
For normal text in HTML content and in quoted attributes, yes, provided you specify ENT_QUOTES and the UTF-8 character set. In JavaScript, in URLs or in CSS, other rules apply, and there a separate, suitable encoding is needed.
Should I filter input on saving or on output?
Encode on output, suited to the destination. On receipt, check input for plausibility, such as length and format. Anyone who filters only on saving overlooks data from other sources and makes later use in other contexts harder.
What does a Content Security Policy do?
A Content Security Policy defines from which sources the browser may load and execute scripts. It can significantly limit the impact of an overlooked XSS vulnerability but does not replace correct encoding.
How do I allow formatting such as bold or links in comments?
Use a vetted HTML sanitising library with a whitelist of permitted elements and attributes. Custom regular expressions for removing scripts are error-prone and should not be used.