12 KiB
12 KiB
WordPress Plugin Security Checklist
Complete security audit checklist for WordPress plugins. Use this when reviewing code for security vulnerabilities.
1. File Access Protection
ABSPATH Check
Required in EVERY PHP file:
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
Why: Prevents direct file access via URL Vulnerability: Remote code execution, information disclosure Source: WordPress Plugin Handbook
✅ Check:
- All
.phpfiles have ABSPATH check - Check is at the top of the file (line 2-4)
- Uses
exitnotdie(WordPress standard)
2. Sanitization (Input Validation)
Always Sanitize User Input
Functions to use:
| Input Type | Sanitization Function |
|---|---|
| Text field | sanitize_text_field() |
| Textarea | sanitize_textarea_field() |
sanitize_email() |
|
| URL | esc_url_raw() |
| File name | sanitize_file_name() |
| HTML content | wp_kses_post() or wp_kses() |
| Integer | absint() or intval() |
| Float | floatval() |
| Key/Slug | sanitize_key() |
| Title | sanitize_title() |
Example:
// ❌ WRONG - No sanitization
$name = $_POST['name'];
// ✅ CORRECT - Sanitized
$name = sanitize_text_field( $_POST['name'] );
✅ Check:
- All
$_POSTvalues are sanitized - All
$_GETvalues are sanitized - All
$_REQUESTvalues are sanitized - All
$_COOKIEvalues are sanitized - Correct sanitization function for data type
3. Escaping (Output Protection)
Always Escape Output
Functions to use:
| Output Context | Escaping Function |
|---|---|
| HTML content | esc_html() |
| HTML attribute | esc_attr() |
| URL | esc_url() |
| JavaScript | esc_js() |
| Textarea | esc_textarea() |
| HTML blocks | wp_kses_post() |
| Translation | esc_html__(), esc_html_e(), esc_attr__(), esc_attr_e() |
Example:
// ❌ WRONG - No escaping
echo $user_input;
echo '<a href="' . $url . '">Link</a>';
// ✅ CORRECT - Escaped
echo esc_html( $user_input );
echo '<a href="' . esc_url( $url ) . '">Link</a>';
✅ Check:
- All variables in HTML are escaped
- All variables in attributes are escaped
- All URLs are escaped
- Correct escaping function for context
4. Nonces (CSRF Protection)
Use Nonces for All Forms and AJAX
Form example:
// Add nonce to form
wp_nonce_field( 'my_action', 'my_nonce_field' );
// Verify nonce when processing
if ( ! wp_verify_nonce( $_POST['my_nonce_field'], 'my_action' ) ) {
wp_die( 'Security check failed' );
}
AJAX example:
// Create nonce
wp_create_nonce( 'my_ajax_nonce' );
// Verify in AJAX handler
check_ajax_referer( 'my_ajax_nonce', 'nonce' );
URL example:
// Add nonce to URL
$url = wp_nonce_url( admin_url( 'admin-post.php?action=my_action' ), 'my_action' );
// Verify nonce
if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'my_action' ) ) {
wp_die( 'Security check failed' );
}
✅ Check:
- All forms have nonce fields
- All form handlers verify nonces
- All AJAX handlers verify nonces
- All admin action URLs have nonces
- Nonce actions are unique and descriptive
5. Capability Checks (Authorization)
Always Check User Permissions
Never use is_admin() - it only checks if you're on an admin page, not user permissions!
Correct:
// ❌ WRONG - Only checks admin area
if ( is_admin() ) {
// Anyone can access this!
}
// ✅ CORRECT - Checks user capability
if ( current_user_can( 'manage_options' ) ) {
// Only admins can access
}
Common capabilities:
| Capability | Who Has It |
|---|---|
manage_options |
Administrator |
edit_posts |
Editor, Author, Contributor |
publish_posts |
Editor, Author |
edit_published_posts |
Editor, Author |
delete_posts |
Editor, Author |
upload_files |
Editor, Author |
read |
All logged-in users |
✅ Check:
- All admin pages check capabilities
- All AJAX handlers check capabilities
- All REST endpoints have permission callbacks
- All form handlers check capabilities
- Never rely on
is_admin()alone
6. SQL Injection Prevention
Always Use Prepared Statements
Use $wpdb->prepare():
global $wpdb;
// ❌ WRONG - SQL injection vulnerability
$results = $wpdb->get_results( "SELECT * FROM table WHERE id = {$id}" );
// ✅ CORRECT - Prepared statement
$results = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM table WHERE id = %d",
$id
) );
Placeholders:
| Type | Placeholder |
|---|---|
| Integer | %d |
| Float | %f |
| String | %s |
✅ Check:
- All
$wpdbqueries useprepare() - Correct placeholder for data type
- Never concatenate variables into SQL
- User input is sanitized before
prepare()
7. Unique Prefixing
Prevent Naming Conflicts
Use 4-5 character prefix for everything:
// Functions
function myplug_init() {}
// Classes
class MyPlug_Admin {}
// Constants
define( 'MYPLUG_VERSION', '1.0.0' );
// Options
get_option( 'myplug_settings' );
// Meta keys
update_post_meta( $id, '_myplug_data', $value );
// AJAX actions
add_action( 'wp_ajax_myplug_action', 'myplug_ajax_handler' );
// REST routes
register_rest_route( 'myplug/v1', '/endpoint', $args );
✅ Check:
- All functions have unique prefix
- All classes have unique prefix
- All constants have unique prefix
- All database options have unique prefix
- All meta keys have unique prefix (start with
_for hidden) - All AJAX actions have unique prefix
- All REST namespaces have unique prefix
8. Asset Loading
Load Assets Conditionally
// ❌ WRONG - Loads everywhere
function bad_enqueue() {
wp_enqueue_script( 'my-script', $url );
}
add_action( 'wp_enqueue_scripts', 'bad_enqueue' );
// ✅ CORRECT - Loads only where needed
function good_enqueue() {
if ( is_singular( 'book' ) ) {
wp_enqueue_script( 'my-script', $url );
}
}
add_action( 'wp_enqueue_scripts', 'good_enqueue' );
✅ Check:
- Scripts load only on needed pages
- Styles load only on needed pages
- Admin assets use
admin_enqueue_scriptshook - Admin assets check
$hookparameter - Dependencies are declared (
array( 'jquery' )) - Versions are set (for cache busting)
9. Data Validation
Validate Before Saving
// Example: Validate select field
$allowed_values = array( 'option1', 'option2', 'option3' );
$value = sanitize_text_field( $_POST['select_field'] );
if ( ! in_array( $value, $allowed_values, true ) ) {
// Invalid value - reject or use default
$value = 'option1';
}
✅ Check:
- Select/radio values validated against allowed values
- Number fields validated for min/max range
- Email fields validated with
is_email() - URLs validated with
esc_url_raw() - File uploads validated for type and size
10. File Upload Security
Validate File Uploads
// Check file type
$allowed_types = array( 'image/jpeg', 'image/png' );
if ( ! in_array( $_FILES['file']['type'], $allowed_types, true ) ) {
wp_die( 'Invalid file type' );
}
// Check file size
$max_size = 5 * 1024 * 1024; // 5MB
if ( $_FILES['file']['size'] > $max_size ) {
wp_die( 'File too large' );
}
// Use WordPress upload handler
$file = $_FILES['file'];
$upload = wp_handle_upload( $file, array( 'test_form' => false ) );
✅ Check:
- File type is validated
- File size is validated
- Uses
wp_handle_upload()ormedia_handle_upload() - File names are sanitized
- User has
upload_filescapability
11. Direct Object Reference
Check Ownership Before Actions
// ❌ WRONG - No ownership check
$post_id = absint( $_POST['post_id'] );
wp_delete_post( $post_id );
// ✅ CORRECT - Check ownership
$post_id = absint( $_POST['post_id'] );
$post = get_post( $post_id );
if ( ! $post || $post->post_author != get_current_user_id() ) {
wp_die( 'Permission denied' );
}
wp_delete_post( $post_id );
✅ Check:
- Delete/edit actions verify ownership
- Or check appropriate capability
- REST endpoints verify ownership in permission callback
12. REST API Security
Secure REST Endpoints
register_rest_route( 'myplugin/v1', '/items', array(
'methods' => 'POST',
'callback' => 'my_callback',
// ✅ REQUIRED: Permission callback
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
// ✅ REQUIRED: Argument validation
'args' => array(
'title' => array(
'required' => true,
'validate_callback' => function( $param ) {
return ! empty( $param );
},
'sanitize_callback' => 'sanitize_text_field',
),
),
) );
✅ Check:
- All endpoints have
permission_callback - Never use
'permission_callback' => '__return_true'for write operations - All parameters have validation
- All parameters have sanitization
- Return proper HTTP status codes (200, 400, 401, 404, 500)
13. Internationalization Security
Escape Translated Strings
// ❌ WRONG - Vulnerable to XSS
echo __( 'Hello', 'my-plugin' );
// ✅ CORRECT - Escaped
echo esc_html__( 'Hello', 'my-plugin' );
echo esc_html_e( 'Hello', 'my-plugin' );
echo esc_attr__( 'Hello', 'my-plugin' );
✅ Check:
- Use
esc_html__()instead of__() - Use
esc_html_e()instead of_e() - Use
esc_attr__()for attributes - Never output
__()directly
14. Data Cleanup
Remove Data on Uninstall (Not Deactivation)
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete options
delete_option( 'myplug_settings' );
// Delete transients
delete_transient( 'myplug_cache' );
// Delete posts (optional - user data loss!)
// Only if absolutely necessary
✅ Check:
- Options deleted in
uninstall.php - Transients deleted in
uninstall.php - Never delete data in deactivation hook
- Consider asking user before deleting post data
15. Common Vulnerabilities to Avoid
XSS (Cross-Site Scripting)
- Never
echouser input without escaping - Never use
innerHTMLwith user data - Always escape in HTML context
SQL Injection
- Never concatenate variables into SQL
- Always use
$wpdb->prepare() - Sanitize before
prepare()
CSRF (Cross-Site Request Forgery)
- All forms have nonces
- All AJAX has nonces
- All admin actions have nonces
Authorization Bypass
- Never use
is_admin()alone - Always check capabilities
- Verify ownership for user-specific data
Path Traversal
- Validate file paths
- Use
realpath()to resolve paths - Never allow
../in file operations
Quick Security Scan
Run this checklist on every file:
- ABSPATH check at top
- Unique prefix on all names
- All
$_POST/$_GETsanitized - All output escaped
- All forms/AJAX have nonces
- All actions check capabilities
- All
$wpdbqueries useprepare() - Assets load conditionally
- No direct file access
- No hardcoded credentials
Resources
- WordPress Security Whitepaper: https://wordpress.org/about/security/
- Plugin Security: https://developer.wordpress.org/apis/security/
- Patchstack Database: https://patchstack.com/database/
- Wordfence: https://www.wordfence.com/blog/
- WPScan: https://wpscan.com/
Last Updated: 2025-11-06 Status: Production Ready