Skip to content

Latest commit

 

History

History
452 lines (321 loc) · 9.48 KB

File metadata and controls

452 lines (321 loc) · 9.48 KB
layout default
title Template Security
parent Advanced
nav_order 2

PHP Template Security Guide

This document describes the security features implemented in the PHP template rendering system to prevent XSS (Cross-Site Scripting) and other injection attacks.


1. Auto-Escaping with SafeContext

1.1 Overview

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

1.2 Usage in Templates

Basic Usage (Auto-Escaped):

// Template context:
// ['username' => '<script>alert("XSS")</script>']

// SAFE - Auto-escaped
echo $context->username;
// Output: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// SAFE - Array access also auto-escapes
echo $context['username'];
// Output: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

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();

1.3 Array Handling

Arrays are recursively escaped:

// Context: ['items' => ['<script>', 'safe', '<img>']]

foreach ($context->items as $item) {
    echo $item; // Each item is auto-escaped
}

// Output:
// &lt;script&gt;
// safe
// &lt;img&gt;

1.4 Type Preservation

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;   // null

2. Context-Aware Escaping

For advanced use cases, the escapeContext() helper provides context-specific escaping.

2.1 Available Contexts

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'); ?>

2.2 Examples

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>

3. Security Best Practices

3.1 Default Rule: Use SafeContext

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! -->

3.2 When to Use raw()

Only use raw() for:

  1. Pre-sanitized HTML (e.g., SQL syntax highlighting from Doctrine)
  2. Trusted content (e.g., generated code examples)
  3. 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>

3.3 Backward Compatibility

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 default

Migration Recommendation: Update templates to use $context-> for new code.


4. Migration Guide

4.1 Updating Existing Templates

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>

4.2 Migration Checklist

  • 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

4.3 Gradual Migration

Templates can use both styles during migration:

// Safe: Both work together
echo $context->username;      // Auto-escaped
echo htmlspecialchars($oldVar, ENT_QUOTES, 'UTF-8'); // Manual escape

5. Common Pitfalls

5.1 Double-Escaping

Problem:

// 📢 BAD - Double-escaped
echo htmlspecialchars($context->username, ENT_QUOTES, 'UTF-8');
// Output: &amp;lt;script&amp;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;

5.2 Forgetting to Escape in Attributes

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'); ?>">

5.3 JavaScript Context Errors

Problem:

// 📢 WRONG - HTML escaping in JavaScript breaks syntax
<script>
    var name = "<?php echo $context->name; ?>";
    // Output: var name = "&lt;script&gt;"; (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>

5.4 Trusting "Safe" User Input

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 role

XSS Prevention Examples

Example 1: Preventing Script Injection

Attack:

Input: <script>alert('XSS')</script>

Defense:

echo $context->input;
// Output: &lt;script&gt;alert(&#039;XSS&#039;)&lt;/script&gt;
// Browser displays: <script>alert('XSS')</script> (as text, not executed)

Example 2: Preventing Attribute Injection

Attack:

Input: " onclick="alert('XSS')

Defense:

<button class="<?php echo $context->input; ?>">Click</button>
// Output: <button class="&quot; onclick=&quot;alert(&#039;XSS&#039;)">Click</button>
// Quote is escaped, attribute injection prevented

Example 3: Preventing URL Injection

Attack:

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 prevented

Testing Security

Unit Tests

All security features are covered by unit tests in tests/Unit/Template/Security/SafeContextTest.php.

Run security tests:

vendor/bin/phpunit --testsuite unit --filter SafeContext

Manual Testing

Test 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

References


[← Back to Main Documentation]({{ site.baseurl }}/) | Architecture →