| layout | default |
|---|---|
| title | Template Security |
| parent | Advanced |
| nav_order | 2 |
This document describes the security features implemented in the PHP template rendering system to prevent XSS (Cross-Site Scripting) and other injection attacks.
The SafeContext class provides automatic HTML escaping for all template variables, similar to Twig's auto-escaping feature.
Security Benefits:
- Prevents XSS by default
- Immutable context prevents variable tampering
- Type-safe access to variables
- Clear separation between safe and unsafe content
Basic Usage (Auto-Escaped):
// Template context:
// ['username' => '<script>alert("XSS")</script>']
// SAFE - Auto-escaped
echo $context->username;
// Output: <script>alert("XSS")</script>
// SAFE - Array access also auto-escapes
echo $context['username'];
// Output: <script>alert("XSS")</script>Raw Output (Intentional Unescaped):
// UNSAFE - Use only for pre-sanitized content
echo $context->raw('formatted_sql');
// Example: SQL syntax highlighting is already sanitized
echo $context->raw('highlighted_code');Conditional Checks:
// Check if variable exists
if ($context->has('suggestion')) {
echo $context->suggestion;
}
// Get all available keys
$keys = $context->keys();Arrays are recursively escaped:
// Context: ['items' => ['<script>', 'safe', '<img>']]
foreach ($context->items as $item) {
echo $item; // Each item is auto-escaped
}
// Output:
// <script>
// safe
// <img>Non-string types are preserved:
// Context:
// [
// 'count' => 42,
// 'price' => 19.99,
// 'active' => true,
// 'data' => null,
// ]
echo $context->count; // 42 (int)
echo $context->price; // 19.99 (float)
echo $context->active; // true (bool)
echo $context->data; // nullFor advanced use cases, the escapeContext() helper provides context-specific escaping.
| Context | Use Case | Example |
|---|---|---|
html |
HTML content (default) | <div><?php echo escapeContext($text, 'html'); ?></div> |
attr |
HTML attributes | <div class="<?php echo escapeContext($class, 'attr'); ?>"> |
js |
JavaScript strings | var name = <?php echo escapeContext($name, 'js'); ?>; |
css |
CSS identifiers | .<?php echo escapeContext($class, 'css'); ?> { } |
url |
URL parameters | ?param=<?php echo escapeContext($value, 'url'); ?> |
HTML Context (Default):
<p><?php echo escapeContext($userInput, 'html'); ?></p>Attribute Context:
<div class="user-<?php echo escapeContext($userId, 'attr'); ?>">
<a href="/profile/<?php echo escapeContext($username, 'url'); ?>">
Profile
</a>
</div>JavaScript Context:
<script>
var config = {
username: <?php echo escapeContext($username, 'js'); ?>,
message: <?php echo escapeContext($message, 'js'); ?>
};
</script>CSS Context:
<style>
.severity-<?php echo escapeContext($severity, 'css'); ?> {
color: red;
}
</style>GOOD:
// Auto-escaped by default
<h3><?php echo $context->title; ?></h3>
<p><?php echo $context->description; ?></p>📢 BAD:
// Manual extraction bypasses auto-escaping
extract($context);
<h3><?php echo $title; ?></h3> <!-- NOT ESCAPED! -->Only use raw() for:
- Pre-sanitized HTML (e.g., SQL syntax highlighting from Doctrine)
- Trusted content (e.g., generated code examples)
- Already-escaped content (avoid double-escaping)
GOOD - Pre-sanitized:
// formatSqlWithHighlight() already escapes and adds HTML
<div class="query-item">
<?php echo $context->raw('formatted_sql'); ?>
</div>📢 BAD - User input:
// NEVER use raw() on user input
<div>
<?php echo $context->raw('user_comment'); ?> <!-- XSS VULNERABILITY! -->
</div>For existing templates using extract(), variables are still available but NOT auto-escaped:
// Old style (still works but NOT auto-escaped)
extract($context);
echo htmlspecialchars($username, ENT_QUOTES, 'UTF-8'); // Manual escape required
// New style (auto-escaped)
echo $context->username; // Safe by defaultMigration Recommendation: Update templates to use $context-> for new code.
Before:
<?php
// Old template using extract()
extract($context);
$e = fn(string $str): string => htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
?>
<div class="alert">
<strong><?php echo $e($title); ?></strong>
<p><?php echo $e($message); ?></p>
</div>After:
<?php
// New template using SafeContext
// No need for manual escaping function
?>
<div class="alert">
<strong><?php echo $context->title; ?></strong>
<p><?php echo $context->message; ?></p>
</div>- Replace
$e($variable)with$context->variable - Remove manual
htmlspecialchars()calls - Use
$context->raw()for pre-sanitized content - Add tests for XSS prevention
- Review all
raw()usage with security team
Templates can use both styles during migration:
// Safe: Both work together
echo $context->username; // Auto-escaped
echo htmlspecialchars($oldVar, ENT_QUOTES, 'UTF-8'); // Manual escapeProblem:
// 📢 BAD - Double-escaped
echo htmlspecialchars($context->username, ENT_QUOTES, 'UTF-8');
// Output: &lt;script&gt; (double-encoded)Solution:
// GOOD - Use raw() to get unescaped value, then escape once
echo htmlspecialchars($context->raw('username'), ENT_QUOTES, 'UTF-8');
// BETTER - Just use auto-escaping
echo $context->username;Problem:
// 📢 VULNERABLE - Attribute injection
<div class="user-<?php echo $context->raw('userId'); ?>">Solution:
// SAFE - Auto-escaped
<div class="user-<?php echo $context->userId; ?>">
// SAFE - Context-aware escaping
<div class="user-<?php echo escapeContext($context->raw('userId'), 'attr'); ?>">Problem:
// 📢 WRONG - HTML escaping in JavaScript breaks syntax
<script>
var name = "<?php echo $context->name; ?>";
// Output: var name = "<script>"; (breaks JS)
</script>Solution:
// CORRECT - Use JS context escaping
<script>
var name = <?php echo escapeContext($context->raw('name'), 'js'); ?>;
// Output: var name = "\u003Cscript\u003E"; (safe and valid JS)
</script>Problem:
// 📢 DANGEROUS ASSUMPTION
// "Admin users are trusted, so we can use raw()"
if ($user->isAdmin()) {
echo $context->raw('comment'); // STILL VULNERABLE!
}Solution:
// PRINCIPLE: Never trust ANY user input
echo $context->comment; // Always escape, regardless of user roleAttack:
Input: <script>alert('XSS')</script>
Defense:
echo $context->input;
// Output: <script>alert('XSS')</script>
// Browser displays: <script>alert('XSS')</script> (as text, not executed)Attack:
Input: " onclick="alert('XSS')
Defense:
<button class="<?php echo $context->input; ?>">Click</button>
// Output: <button class="" onclick="alert('XSS')">Click</button>
// Quote is escaped, attribute injection preventedAttack:
Input: javascript:alert('XSS')
Defense:
<a href="<?php echo escapeContext($context->raw('url'), 'url'); ?>">Link</a>
// Output: <a href="javascript%3Aalert%28%27XSS%27%29">Link</a>
// URL is encoded, script execution preventedAll security features are covered by unit tests in tests/Unit/Template/Security/SafeContextTest.php.
Run security tests:
vendor/bin/phpunit --testsuite unit --filter SafeContextTest XSS Prevention:
// Create test context with XSS payloads
$context = new SafeContext([
'test1' => '<script>alert(1)</script>',
'test2' => '<img src=x onerror=alert(1)>',
'test3' => 'javascript:alert(1)',
]);
// Verify all are escaped
var_dump($context->test1); // Should NOT contain executable script
var_dump($context->test2); // Should NOT contain onerror handler
var_dump($context->test3); // Should NOT contain javascript: protocol- OWASP XSS Prevention Cheat Sheet
- PHP htmlspecialchars() Documentation
- Twig Auto-Escaping Strategy
- Content Security Policy (CSP)
[← Back to Main Documentation]({{ site.baseurl }}/) | Architecture →