FlawOpen

flawopen.com/xss/Php

● CWE-79 · High
FlawOpen Security Research

PHP XSS: Escape Output in Blade, Twig and PHP

Escape HTML with htmlspecialchars(), encode script data with safe json_encode() flags, and sanitize rich text with an allow-list (CWE-79).

💡 Plain English Explainer (ELI5)

A restaurant’s digital menu shows customer-submitted dish descriptions with the restaurant’s branding. Blade’s {{ }} slot escapes a description so < appears as a character; {!! !!} switches to trusting the value as ready-made HTML, and raw PHP echo can bypass the template too. The browser parses the page as text/html from the restaurant’s origin, so inserted markup inherits the site’s authority. Escape by output context with htmlspecialchars; sanitize only when rich HTML is a real requirement.

Core Concepts & Subsystem Terms

htmlspecialchars() flags
Use htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8'). ENT_QUOTES encodes both quote characters (the default since PHP 8.1, but not before), and ENT_SUBSTITUTE replaces invalid UTF-8 instead of returning an empty string. Wrap it in a short helper so every call uses the same flags.
Unquoted attributes
Encoding quotes only helps inside a quoted attribute. In alt=$name there are no quotes to break out of: a space followed by onerror=alert(1) adds a new attribute. Always quote attribute values with double quotes.
json_encode() inside <script>
A JSON string can contain </script>, which ends the script block regardless of JavaScript syntax. json_encode escapes / by default, but JSON_UNESCAPED_SLASHES turns that off. JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT encodes < > & ' " as \u003C and friends.
Blade {{ }} vs {!! !!}
Blade's {{ $x }} runs htmlspecialchars for you. {!! $x !!} prints raw HTML and should only ever receive sanitized content. Twig autoescapes the same way, and its |raw filter is the equivalent opt-out.
HTML Purifier
An allowlist HTML sanitizer that parses the input, removes anything not permitted (scripts, event handlers, javascript: URLs, unknown tags) and outputs standards-compliant markup. Use it when users are allowed to submit formatted text.

Framework mappings

Step-by-Step Attack Flow

Step 1

Input arrives in `$_GET` or `$_POST`

A search link carries q, a profile form posts name and bio, and a settings page embeds the user record in a script. A victim can be sent a crafted link, or a stored bio is shown to every visitor.

Step 2

The value lands unescaped

A double-quoted string interpolates it, an unquoted alt= lets a space start a new attribute, JSON_UNESCAPED_SLASHES keeps </script> intact, or the bio is echoed raw. The benign test value <img src=x onerror=alert(1)> survives each path.

Step 3

The browser parses it as markup

The page contains a real <img> element or a new onerror attribute; in the script case, </script> closes the block early and the rest is parsed as HTML.

Step 4

The script runs as the site

The code executes with the site's origin: it can read the DOM, call the site's endpoints with the victim's PHPSESSID cookie and change what the user sees.

Source Code: Flaw vs. Secure Implementation

UNPATCHED FLAW
<?php
// views.php: each function below lets request text reach the page as HTML

// the double-quoted string interpolates q unescaped
function renderSearch(string $q): string
{
    return "<h1>Results for $q</h1>";
}

// without quotes, a space in name starts a new attribute
function renderAvatar(string $name): string
{
    return "<img alt=" . htmlspecialchars($name) . " src=\"/avatar.png\">";
}

// unescaped slashes let a value contain </script>
function renderConfig(array $user): string
{
    return "<script>const user = " . json_encode($user, JSON_UNESCAPED_SLASHES) . ";</script>";
}

// the bio is echoed with every tag and handler intact
function renderBio(string $bio): string
{
    return "<div class=\"bio\">$bio</div>";
}
HARDENED SECURE PATCH
<?php
// views.php: one escaping helper, quoted attributes, safe JSON and sanitized HTML

function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}

// e() encodes the query for element content
function renderSearch(string $q): string
{
    return '<h1>Results for ' . e($q) . '</h1>';
}

// the attribute is quoted and its value encoded
function renderAvatar(string $name): string
{
    return '<img alt="' . e($name) . '" src="/avatar.png">';
}

// the HEX flags encode < > & ' " so </script> cannot appear
function renderConfig(array $user): string
{
    $flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
    return '<script>const user = ' . json_encode($user, $flags) . ';</script>';
}

// HTML Purifier keeps an allowlist of tags and attributes
function renderBio(string $bio): string
{
    $purifier = new HTMLPurifier(HTMLPurifier_Config::createDefault());
    return '<div class="bio">' . $purifier->purify($bio) . '</div>';
}

Engineering & System Hardening Checklist

Sources