web php
Output Escaping
XSS, or cross-site scripting, happens when untrusted content is rendered into a page in a way the browser treats as code. In PHP applications it usually appears in comments, profile names, search results, admin screens, CMS fields, error pages, and any place that prints data that originally came from a user or third-party system.
There are three common forms. Reflected XSS sends the payload in the request and reflects it immediately, for example a search page that prints the search term. Stored XSS saves the payload, for example in a comment or profile field, and attacks later visitors. DOM-based XSS happens when browser-side JavaScript reads unsafe data and writes it into the DOM.
The main rule is to escape output for the context where it is rendered. Do not rely on strip_tags() as a security control, and do not think that validating input once makes output safe forever. A name that is safe for storage can still be unsafe when placed inside HTML, an attribute, JavaScript, CSS, or a URL.
Use htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') for ordinary HTML output. Use json_encode() when moving PHP data into JavaScript. Validate URLs before rendering href values. If the product needs rich text, use a real HTML sanitizer and a strict allow-list. Content Security Policy helps reduce impact, but it is defence-in-depth, not a replacement for escaping.
Escape for the HTML body
<?php
declare(strict_types=1);
function escapeHtml(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$comment = '<script>alert("xss")</script>';
// Bad: user-controlled HTML is executed by the browser.
// echo "<p>$comment</p>";
// Good: safe for normal HTML body and attribute output.
echo '<p>' . escapeHtml($comment) . '</p>';
echo '<input name="comment" value="' . escapeHtml($comment) . '">';
// Prints:
// <p>&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</p><input name="comment" value="&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;">
ENT_QUOTES escapes both single and double quotes, which matters for attributes. ENT_SUBSTITUTE replaces invalid UTF-8 sequences instead of failing in awkward ways.
JavaScript needs JSON encoding
Do not drop raw PHP strings into JavaScript. Encode data as JSON.
<?php
declare(strict_types=1);
$username = '</script><script>alert(1)</script>';
$script = '<script>window.username = '
. json_encode($username, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT)
. ';</script>';
echo $script . PHP_EOL;
// Prints:
// <script>window.username = "\u003C\/script\u003E\u003Cscript\u003Ealert(1)\u003C\/script\u003E";</script>
The browser receives a JavaScript string value, not executable script markup from the user.
URLs need validation
Escaping a URL is not enough if the URL uses a dangerous scheme. Validate the scheme before rendering a link.
<?php
declare(strict_types=1);
function safeHttpUrl(string $url): ?string
{
$scheme = parse_url($url, PHP_URL_SCHEME);
return in_array($scheme, ['http', 'https'], true) ? $url : null;
}
$url = safeHttpUrl('javascript:alert(1)');
echo $url ?? 'blocked';
// Prints:
// blocked
If the URL is allowed, still escape it before placing it in an href attribute.
Rich text needs sanitizing
Sometimes a product intentionally allows limited HTML, such as bold text or links in a CMS field. Do not solve that with strip_tags() and hope. Use a maintained HTML sanitizer with a strict allow-list of tags, attributes, and protocols.
Admin screens still count
Do not skip escaping because a page is "only for admins". Stored XSS in an admin screen can be worse than public-page XSS because admin users often have powerful sessions. HttpOnly cookies reduce the chance of JavaScript reading a session cookie, but they do not stop injected JavaScript from performing actions as the logged-in user.
What to check in a project
Check where values are rendered, not only where they are stored. A database value can be safe for storage and unsafe for HTML.
Check the context: HTML body, HTML attribute, JavaScript, URL, CSS, or rich text. Each context has different rules.
Check the template engine. Some engines escape by default, but they usually also provide a raw-output escape hatch. Raw output should be rare and justified.
Check internal dashboards and admin pages. They are often where stored XSS bugs hide.
What you should be able to do
After this lesson, you should be able to escape normal HTML output with htmlspecialchars(), pass PHP values into JavaScript with JSON encoding, validate URL schemes before rendering links, and explain why escaping belongs at output time.
Practice
Task: Render Safe Comment Output
Build a small comment renderer. First write an unsafe version in a comment or separate function, then write the safe version that displays user content as text using htmlspecialchars() with ENT_QUOTES | ENT_SUBSTITUTE and UTF-8.
Requirements
- Include malicious-looking input such as
<script>alert(1)</script>. - Show safe output where tags are visible as text, not executed.
- Use a reusable escaping helper for HTML body/attribute output.
- Include a quoted attribute value to prove quotes are escaped.
- Add a note explaining why escaping belongs at output time.
Check Your Work
Run the script and confirm that <, >, and quotes appear escaped in the output.
Show solution
This solution renders the same untrusted comment in an HTML body and an attribute.
<?php
declare(strict_types=1);
function escapeHtml(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$comment = '<script>alert("xss")</script>';
// Bad: user-controlled HTML is executed by the browser.
// echo "<p>$comment</p>";
// Good: safe for normal HTML body and attribute output.
echo '<p>' . escapeHtml($comment) . '</p>' . PHP_EOL;
echo '<input name="comment" value="' . escapeHtml($comment) . '">' . PHP_EOL;
// Prints:
// <p>&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</p>
// <input name="comment" value="&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;">
The important result is that the browser receives escaped text. The payload is visible to the user as text, but it is not interpreted as a script or HTML element.
Why This Works
Escaping happens at the output point, where the code knows the value is going into HTML. The same stored comment might need different handling if it were placed inside JavaScript, a URL, or rich text.