PHP cookbook · Beginner

Prevent XSS in PHP templates

Render user-generated content (names, comments, links) safely in HTML pages.

Approach

Escape every dynamic value at output time for its context. Use htmlspecialchars() with ENT_QUOTES | ENT_SUBSTITUTE and UTF-8 for HTML text and attributes, validate URL schemes for links, and use json_encode with hex flags when passing data to JavaScript. Prefer a templating engine with auto-escaping (Twig, Blade) and add a CSP header.

Solution

<?php
function e(?string $v): string { return htmlspecialchars($v ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }

function safe_url(string $url): string
{
    $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
    return in_array($scheme, ['http', 'https', 'mailto'], true) ? $url : '#';
}

header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'");
?>
<article>
  <h2><?= e($comment['author']) ?></h2>
  <p><?= nl2br(e($comment['body'])) ?></p>
  <a href="<?= e(safe_url($comment['website'])) ?>" rel="nofollow noopener">Website</a>
</article>
<script>
  window.__DATA__ = <?= json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR) ?>;
</script>

Pitfalls

  • Escaping before storing (double-escaping and wrong context later).
  • javascript: URLs in href attributes.
  • Bypassing auto-escaping with |raw or {!! !!} for user content.

Variations

  • Allow limited formatting with Markdown + an HTML sanitizer (Symfony HtmlSanitizer, HTML Purifier).

Further reading: OWASP XSS Prevention Cheat Sheet

Reference: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html

More PHP recipes