Safeguard
Application Security•

How do you prevent cross-site scripting in a PHP web app?

Encode every variable for the context it lands in, use htmlspecialchars with explicit flags and UTF-8, sanitize user HTML with a library, and add a CSP.

Safeguard Security Team
4 min read

In PHP, prevent cross-site scripting by encoding every untrusted value at the moment it is written to a page, using the encoder that matches the context. For HTML text and quoted attributes, that means htmlspecialchars with explicit quote flags and UTF-8. Add sanitization for user-authored HTML and a content security policy as a second layer.

What does htmlspecialchars really do?

It converts five characters: ampersand, double quote, single quote, less-than and greater-than. The PHP manual points out two traps that matter for security.

First, defaults changed. From PHP 8.1 the default flags are ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401. Before 8.1 the default was ENT_COMPAT, which leaves single quotes alone. A single-quoted attribute built on an old default is injectable.

Second, passing your own flags replaces the whole default. The manual's example: calling it with only ENT_HTML5 silently uses ENT_NOQUOTES. A one-argument "improvement" can switch quote encoding off.

Write it the same way everywhere:

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

Name the encoding. The manual strongly recommends passing it explicitly, and invalid input for the stated encoding returns an empty string unless a substitute flag is set.

Is encoding the same in every place on the page?

No, and this is where PHP apps leak. OWASP's cheat sheet separates contexts:

  • HTML body: entity encode the five characters.
  • HTML attributes: always quote the value, and keep attribute names fixed. Encoding does not make event handler attributes such as onclick safe.
  • JavaScript: do not build script from user input. Pass data as JSON in a data attribute or a JSON response and read it with safe DOM calls.
  • URLs: percent-encode parameter values, not the whole URL, then HTML-encode when placing it in an href. Validate the scheme, since a javascript: URL survives encoding.
  • CSS: put variables only in property values from an allowlist.

OWASP also lists contexts with no safe encoding: directly inside script or style tags, inside HTML comments, and in attribute or tag names.

Constructed example: a search page

// Unsafe
echo "<p>Results for " . $_GET['q'] . "</p>";

// Safe for the HTML body
echo "<p>Results for " . e($_GET['q']) . "</p>";

Now the same value in a link: href="/search?q= followed by the value. Use rawurlencode on the value first, then e() on the whole attribute string. Two encoders, in that order, because the value passes through two parsers.

When do you need sanitizing instead of encoding?

If users may submit formatted HTML, such as a comment editor, encoding would show raw tags. OWASP recommends a dedicated sanitizer, naming DOMPurify for browser-side work, and warns not to modify the content after sanitizing. Run the sanitizer last, store the result, and keep the library patched.

Where do PHP templates help?

A template engine with automatic escaping removes the need to remember. Use it, and treat every raw or unescaped output switch as a code review item. Template injection and outdated plugins also appear in OWASP's list of risks, so keep third-party packages current.

What does CSP add?

A content security policy limits where scripts may load from and can block inline script. OWASP calls CSP useful as an extra layer, customized per application, not a replacement for encoding. It also says WAFs are unreliable for XSS, and that input filters in HTTP interceptors cannot know the output context, so they cause double encoding and misses.

What to do

  1. Create one encoder helper and ban direct echo of request, database or API data.
  2. Always pass quote flags and UTF-8 to htmlspecialchars.
  3. Quote every attribute value in templates.
  4. Never insert user data into script blocks, style blocks or event handlers.
  5. Sanitize any user-supplied HTML with a maintained library, as the last step.
  6. Treat data from your own database as untrusted at render time.
  7. Add a CSP, then test with it enforced in staging.
  8. Scan your Composer dependencies for known flaws on every pull request.

Quick check

Is strip_tags enough to stop XSS?

No. It is not a context-aware encoder, and the output may still land in an attribute or URL where other characters matter. Encode at output instead.

Should I encode on input and store clean data?

No. Encode at output, because the right encoding depends on where the value is printed. Store the original text.

Sources

Last reviewed: 18 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.