WordPress Sanitize vs Escape: Which Function to Use for Input and Output Security

WordPress has two categories of data-handling functions that developers often confuse: sanitization functions (which clean data coming in before it is stored) and escaping functions (which encode data going out before it is output). They are not interchangeable — using an escaping function to sanitize a database value is wrong, and using a sanitization function instead of an escaping function when outputting HTML creates XSS vulnerabilities. The rule is simple but must be applied consistently: sanitize on input, escape on output, and always at the last possible moment. WordPress provides a rich set of both types, each tuned for a specific data context: text fields, URLs, email addresses, file names, SQL, HTML content, HTML attributes, JavaScript, and CSS. Knowing which function to use in which context is one of the most important skills for writing secure WordPress code.

Problem: Your plugin saves user-supplied data from a form (text, URL, email, rich HTML) to the database and later outputs it in different HTML contexts (attribute values, tag content, JavaScript). Using the wrong function in any step creates either data corruption or a security vulnerability.

Solution: Apply the correct sanitize function for the data type on save, and the correct escape function for the output context on display. The two operations are always separate — one does not replace the other.

<?php
// ══════════════════════════════════════════
//  SANITIZE — clean data BEFORE storing
// ══════════════════════════════════════════

// Plain text (strip tags, trim whitespace, convert special chars)
$name = sanitize_text_field( $_POST['name'] ?? '' );

// Multi-line plain text (preserves newlines, strips tags)
$bio = sanitize_textarea_field( $_POST['bio'] ?? '' );

// URL (strips invalid URL characters, allows only safe protocols)
$url = esc_url_raw( $_POST['url'] ?? '' );  // for storage (no encoding)

// Email
$email = sanitize_email( $_POST['email'] ?? '' );

// File name (strips path separators and special characters)
$filename = sanitize_file_name( $_POST['filename'] ?? '' );

// CSS class name or HTML ID
$css_class = sanitize_html_class( $_POST['class'] ?? '' );

// Integer
$count = absint( $_POST['count'] ?? 0 );

// Key/slug (lowercase letters, numbers, dashes, underscores)
$key = sanitize_key( $_POST['key'] ?? '' );

// Allowed HTML (rich content — post body, widget text)
$content = wp_kses_post( $_POST['content'] ?? '' );  // allows all post-safe HTML

// Custom allowed tags (e.g. only , , )
$allowed = [
    'strong' => [],
    'em'     => [],
    'a'      => [ 'href' => true, 'title' => true, 'target' => true ],
];
$filtered = wp_kses( $_POST['note'] ?? '', $allowed );

// ══════════════════════════════════════════
//  ESCAPE — encode data BEFORE outputting
// ══════════════════════════════════════════

$name    = get_option( 'my_plugin_name' );
$url     = get_option( 'my_plugin_url' );
$content = get_option( 'my_plugin_content' );
$data    = [ 'key' => 'value with "quotes" & symbols' ];

// Inside HTML tag content
echo esc_html( $name );

// Inside HTML attribute value
echo '<input value="' . esc_attr( $name ) . '">';

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

// In a <script> block or wp_localize_script / wp_add_inline_script
echo '<script>var data = ' . wp_json_encode( $data ) . ';</script>';

// In a <style> block (CSS value)
echo '<style>body { color: ' . esc_attr( '#333333' ) . '; }</style>';

// Translated string going directly to HTML output
esc_html_e( 'Save settings', 'textdomain' );
$label = esc_html__( 'Save settings', 'textdomain' );

// In a textarea
echo '<textarea>' . esc_textarea( $content ) . '</textarea>';

NOTE: esc_url() is for HTML output (encodes the URL for an attribute value) — use esc_url_raw() when sanitizing a URL for database storage, redirects, or HTTP headers, since those contexts do not need HTML encoding. Do not double-escape: if you call esc_html() on a value that was already HTML-encoded on the way in, you will get double-encoded output like &amp;lt;. Sanitize on input and escape on output, never both. wp_kses_post() allows the same HTML tags that WordPress allows in post content — it is appropriate for widget text, excerpt overrides, and any user-controlled rich text outside of the_content.