flawopen.com/xss/Php
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).
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_QUOTESencodes both quote characters (the default since PHP 8.1, but not before), andENT_SUBSTITUTEreplaces 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=$namethere are no quotes to break out of: a space followed byonerror=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_encodeescapes/by default, butJSON_UNESCAPED_SLASHESturns that off.JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOTencodes< > & ' "as\u003Cand friends. Blade {{ }} vs {!! !!}- Blade's
{{ $x }}runshtmlspecialcharsfor you.{!! $x !!}prints raw HTML and should only ever receive sanitized content. Twig autoescapes the same way, and its|rawfilter 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
- CWE reference for the weakness described in this guide: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
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.
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.
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.
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
<?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>";
}
<?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
- ✓Run Psalm taint analysis (
--taint-analysis) in CI and fail onTaintedHtml, and grep Blade templates for{!!and Twig templates for|raw. - ✓Escape every value with one helper that calls
htmlspecialcharswithENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5andUTF-8, and always quote attributes. - ✓Embed data in scripts only with
json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR), or pass it through adata-attribute. - ✓Pass user-supplied HTML through HTML Purifier before it is printed with
{!! !!},|raworecho. - ✓Send a Content-Security-Policy with a nonce-based
script-src, and setsession.cookie_httponly = 1andsession.cookie_samesite = Laxin php.ini.