Add a Secure Product Meta Box to WooCommerce with Nonce Verification and Sanitization

WooCommerce product meta boxes allow store managers to attach additional information to products — things like fabric composition, warranty period, compatible models, or supplier reference codes — without creating a full custom post type. Every value a user types into an admin form must be treated as untrusted input, even inside the WordPress admin panel, because administrator accounts can be compromised and stored XSS via a meta value can execute on every page that renders it. Adding a meta box with add_meta_box() on the add_meta_boxes hook scopes the box to the product edit screen cleanly without touching WooCommerce core. The save routine must verify the nonce created in the meta box render function before processing any input — a missing nonce check exposes the save handler to cross-site request forgery attacks from other admin pages. Checking current_user_can('edit_products') before saving ensures that shop managers with limited capabilities cannot save data that editor-level users should not control. Using sanitize_text_field() for plain text and wp_kses_post() for HTML values strips dangerous tags while preserving intended formatting. Storing the cleaned value with update_post_meta() and retrieving it with get_post_meta() then outputting through esc_html() in the template completes the full sanitize-store-escape cycle. Adding a sanitization filter to the meta value with add_filter('sanitize_post_meta_') enforces the rule automatically for every code path that saves the meta, not only the explicit save handler. The nonce and sanitization post covers the same security principles for AJAX endpoints — the pattern is identical to the meta box save handler described here. The WooCommerce access control guide demonstrates how capability checks integrate with product editing to limit which roles can see and modify sensitive product fields. Run a test with a manually crafted <script>alert(1)</script> value in the field after implementing sanitization to confirm the output is escaped before trusting the implementation in production.

Problem: WooCommerce product meta boxes that skip nonce verification, capability checks, and input sanitization allow stored XSS payloads to be saved and rendered on the product page for every visitor.

Solution: Add a meta box with add_meta_box(), verify the nonce and user capability in the save handler, sanitize with sanitize_text_field(), and escape with esc_html() on every output.

// 1. Register the meta box on the product edit screen
add_action('add_meta_boxes', function() {
    add_meta_box(
        'product_warranty_box',
        'Warranty Information',
        'render_warranty_meta_box',
        'product',
        'side',
        'default'
    );
});

// 2. Render the meta box HTML
function render_warranty_meta_box(WP_Post $post): void {
    wp_nonce_field('save_warranty_data', 'warranty_nonce');
    $value = get_post_meta($post->ID, '_warranty_period', true);
    echo '<label for="warranty_period">Warranty period (e.g. 2 years)</label>';
    echo '<input type="text" id="warranty_period" name="warranty_period" '
         . 'value="' . esc_attr($value) . '" style="width:100%">';
}

// 3. Save with nonce check, capability check, and sanitization
add_action('save_post_product', function(int $post_id): void {
    if (!isset($_POST['warranty_nonce'])) return;
    if (!wp_verify_nonce($_POST['warranty_nonce'], 'save_warranty_data')) return;
    if (!current_user_can('edit_products')) return;
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;

    if (isset($_POST['warranty_period'])) {
        $clean = sanitize_text_field(wp_unslash($_POST['warranty_period']));
        update_post_meta($post_id, '_warranty_period', $clean);
    }
});

// 4. Output in the product template (single-product/meta.php override)
$warranty = get_post_meta(get_the_ID(), '_warranty_period', true);
if ($warranty) {
    echo '<span class="warranty-period">' . esc_html($warranty) . '</span>';
}

NOTE: Pass the post ID through wp_is_post_revision() at the top of the save handler and return early if it is a revision — revisions fire save_post_product too, and saving meta on a revision stores it against the wrong post ID.