# Wordpress Security

> This skill should be used when the user asks to "secure a WordPress plugin", "escape output", "sanitize input", "verify nonces", "check user capabilities", "write safe database queries", or mentions "WordPress security", "esc_html", "sanitize_text_field", "wp_nonce", "wpdb prepare", "XSS", "CSRF", "SQL injection", "capability check", "wp_kses". Provides WordPress security best practices including output escaping, input sanitization, nonce verification, capability checks, and secure database queries.

- Skill: `iwritec0de/wordpress-security` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add iwritec0de/wordpress-security`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iwritec0de/wordpress-security/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- License: MIT
- Author: iwritec0de (https://skillmd.com/u/iwritec0de)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iwritec0de/wordpress-security

---


# WordPress Security Best Practices

This skill covers WordPress security patterns including output escaping, input sanitization, nonce verification, capability checks, and secure database queries.

## Output Escaping

Every piece of dynamic data rendered in HTML must be escaped. Choose the function matching the output context:

| Context | Function | Example |
|---------|----------|---------|
| HTML body | `esc_html()` | `<p><?php echo esc_html( $name ); ?></p>` |
| HTML attribute | `esc_attr()` | `<input value="<?php echo esc_attr( $val ); ?>">` |
| URL/href | `esc_url()` | `<a href="<?php echo esc_url( $link ); ?>">` |
| Textarea content | `esc_textarea()` | `<textarea><?php echo esc_textarea( $text ); ?></textarea>` |
| Inline JS value | `esc_js()` | `onclick="alert('<?php echo esc_js( $msg ); ?>')"` |
| Rich HTML (post content) | `wp_kses_post()` | `<?php echo wp_kses_post( $content ); ?>` |
| Custom allowed HTML | `wp_kses()` | `echo wp_kses( $html, $allowed_tags );` |

**Translation + escaping combos:**
- `esc_html__()` / `esc_html_e()` — translatable escaped strings
- `esc_attr__()` / `esc_attr_e()` — translatable escaped attributes
- `wp_kses_post()` on `__()` output for rich translated content

## Input Sanitization

Sanitize all user input immediately upon receipt, before storage or processing:

| Data Type | Function |
|-----------|----------|
| Plain text | `sanitize_text_field( wp_unslash( $_POST['field'] ) )` |
| Textarea | `sanitize_textarea_field( wp_unslash( $_POST['field'] ) )` |
| Email | `sanitize_email( $_POST['email'] )` |
| Integer | `absint( $_POST['id'] )` or `intval( $_POST['num'] )` |
| Filename | `sanitize_file_name( $_FILES['file']['name'] )` |
| HTML class | `sanitize_html_class( $_POST['class'] )` |
| Key/slug | `sanitize_key( $_POST['key'] )` |
| Title/slug | `sanitize_title( $_POST['title'] )` |
| URL | `esc_url_raw( $_POST['url'] )` (for storage — use `esc_url()` for display) |

**Always `wp_unslash()` superglobals** before sanitizing — WordPress adds slashes to `$_GET`, `$_POST`, `$_REQUEST`.

## Nonce Verification

Every form submission and AJAX request must include a nonce for CSRF protection:

### Forms
```php
// In the form template:
wp_nonce_field( 'myplugin_save_action', 'myplugin_nonce' );

// In the handler:
if ( ! isset( $_POST['myplugin_nonce'] ) ||
     ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['myplugin_nonce'] ) ), 'myplugin_save_action' ) ) {
    wp_die( esc_html__( 'Security check failed.', 'myplugin' ) );
}
```

### AJAX
```php
// Enqueue with nonce:
wp_localize_script( 'myplugin-script', 'myPluginAjax', array(
    'nonce' => wp_create_nonce( 'myplugin_ajax_nonce' ),
    'url'   => admin_url( 'admin-ajax.php' ),
) );

// In the AJAX handler:
check_ajax_referer( 'myplugin_ajax_nonce', 'nonce' );
```

### Admin pages
```php
check_admin_referer( 'myplugin_settings_action', 'myplugin_settings_nonce' );
```

## Capability Checks

Always verify the current user has permission before performing privileged operations:

```php
// Before saving settings:
if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( esc_html__( 'Unauthorized.', 'myplugin' ) );
}

// Before editing posts:
if ( ! current_user_can( 'edit_post', $post_id ) ) {
    wp_die( esc_html__( 'Unauthorized.', 'myplugin' ) );
}
```

Common capabilities: `manage_options`, `edit_posts`, `publish_posts`, `edit_others_posts`, `delete_posts`, `upload_files`, `manage_categories`, `edit_users`.

## Database Security

Never pass unsanitized data into SQL. Always use `$wpdb->prepare()`:

```php
global $wpdb;

// Parameterized query:
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}custom_table WHERE user_id = %d AND status = %s",
        $user_id,
        $status
    )
);

// Prefer CRUD methods for single-row operations:
$wpdb->insert( $wpdb->prefix . 'custom_table', array(
    'user_id' => $user_id,
    'status'  => $status,
), array( '%d', '%s' ) );

$wpdb->update( $wpdb->prefix . 'custom_table',
    array( 'status' => 'active' ),
    array( 'id' => $row_id ),
    array( '%s' ),
    array( '%d' )
);

$wpdb->delete( $wpdb->prefix . 'custom_table',
    array( 'id' => $row_id ),
    array( '%d' )
);
```

## File Operations

Use the WP_Filesystem API instead of direct PHP file functions:

```php
global $wp_filesystem;
WP_Filesystem();

$wp_filesystem->put_contents( $file_path, $content, FS_CHMOD_FILE );
$content = $wp_filesystem->get_contents( $file_path );
```

For uploads, use `wp_handle_upload()`:

```php
$uploaded = wp_handle_upload( $_FILES['myfile'], array( 'test_form' => false ) );
if ( isset( $uploaded['error'] ) ) {
    wp_die( esc_html( $uploaded['error'] ) );
}
```

## AJAX & REST Security

### AJAX handlers
```php
add_action( 'wp_ajax_myplugin_action', 'myplugin_ajax_handler' );

function myplugin_ajax_handler(): void {
    check_ajax_referer( 'myplugin_nonce', 'nonce' );

    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_send_json_error( 'Unauthorized', 403 );
    }

    $data = sanitize_text_field( wp_unslash( $_POST['data'] ) );
    // ... process ...

    wp_send_json_success( array( 'result' => $data ) );
}
```

### REST endpoints
```php
register_rest_route( 'myplugin/v1', '/items', array(
    'methods'             => 'GET',
    'callback'            => 'myplugin_get_items',
    'permission_callback' => function (): bool {
        return current_user_can( 'read' );
    },
) );
```

**Never set `permission_callback` to `__return_true`** unless the endpoint is intentionally public.

## Redirects & JSON Responses

```php
// Safe redirect (restricts to allowed hosts):
wp_safe_redirect( admin_url( 'admin.php?page=myplugin' ) );
exit;

// JSON responses:
wp_send_json_success( $data );
wp_send_json_error( $message, $status_code );

// Terminate execution properly:
wp_die( $message, $title, array( 'response' => 403 ) );
```

For the full function reference with signatures and examples, see `references/security-functions.md`.

