Security Guidelines for Wikit Development
Overview
Security is paramount in WordPress development. All Wikit projects MUST follow these security guidelines to protect against common vulnerabilities. These practices are non-negotiable and must be applied consistently across all code.
Core Security Principles
1. Never Trust User Input
All data from users, URLs, cookies, or external sources must be validated and sanitized.
2. Escape Late, Escape Often
Escape data as close to output as possible, even if already escaped elsewhere.
3. Validate Early
Check and validate input data before processing.
4. Use WordPress APIs
WordPress provides battle-tested security functions - always use them.
Input Validation and Sanitization
Sanitizing User Input
php
// CORRECT: Sanitize all $_POST, $_GET, $_REQUEST data
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
$title = isset( $_POST['title'] ) ? sanitize_text_field( $_POST['title'] ) : '';
$email = isset( $_POST['email'] ) ? sanitize_email( $_POST['email'] ) : '';
$url = isset( $_POST['url'] ) ? esc_url_raw( $_POST['url'] ) : '';
$content = isset( $_POST['content'] ) ? wp_kses_post( $_POST['content'] ) : '';
// CORRECT: Sanitize with validation
$status = isset( $_POST['status'] ) ? sanitize_key( $_POST['status'] ) : 'draft';
if ( ! in_array( $status, [ 'draft', 'publish', 'pending' ], true ) ) {
$status = 'draft';
}
// INCORRECT: Never use raw input
$title = $_POST['title']; // VULNERABLE!Common Sanitization Functions
| Function | Use Case | Example |
|---|---|---|
sanitize_text_field() | Plain text fields | Titles, names |
sanitize_textarea_field() | Multi-line plain text | Comments, descriptions |
sanitize_email() | Email addresses | User email |
sanitize_file_name() | File names | Upload names |
sanitize_key() | Database keys | Meta keys, options |
sanitize_title() | URL slugs | Post slugs |
absint() | Positive integers | IDs, counts |
intval() | Any integer | Offsets, limits |
wp_kses_post() | HTML content | Post content |
esc_url_raw() | URLs for storage | Database URLs |
Output Escaping
Always Escape Output
php
// CORRECT: Escape all output
<h1><?php echo esc_html( $title ); ?></h1>
<a href="<?php echo esc_url( $link ); ?>" class="<?php echo esc_attr( $class ); ?>">
<?php echo esc_html( $text ); ?>
</a>
// CORRECT: Using WordPress functions that auto-escape
<?php the_title(); // Auto-escaped ?>
<?php the_content(); // Filtered through kses ?>
// CORRECT: Escaping in attributes
<input
type="text"
name="<?php echo esc_attr( $name ); ?>"
value="<?php echo esc_attr( $value ); ?>"
data-id="<?php echo esc_attr( $id ); ?>"
>
// INCORRECT: Unescaped output
<h1><?php echo $title; ?></h1> <!-- VULNERABLE! -->Escaping Functions Reference
| Function | Use Case | Context |
|---|---|---|
esc_html() | HTML text content | <p>text</p> |
esc_attr() | HTML attributes | class="" |
esc_url() | URLs in attributes | href="" |
esc_js() | Inline JavaScript | onclick="" |
esc_textarea() | Textarea content | <textarea> |
wp_kses() | Custom HTML filtering | Rich content |
wp_kses_post() | Post content HTML | User content |
Complex Escaping Patterns
php
// CORRECT: Escaping with sprintf
$html = sprintf(
'<a href="%s" class="%s" title="%s">%s</a>',
esc_url( $url ),
esc_attr( $class ),
esc_attr( $title ),
esc_html( $text )
);
// CORRECT: Escaping JSON for JavaScript
<script>
var data = <?php echo wp_json_encode( $data ); ?>;
</script>
// CORRECT: Escaping with allowed HTML
$allowed_html = [
'a' => [
'href' => [],
'title' => [],
],
'br' => [],
'em' => [],
'strong' => [],
];
echo wp_kses( $content, $allowed_html );Nonce Verification
Form Nonces
php
// CREATING: In your form
<form method="post">
<?php wp_nonce_field( 'my_action_nonce', 'security' ); ?>
<!-- form fields -->
</form>
// VERIFYING: In form handler
if ( ! isset( $_POST['security'] ) || ! wp_verify_nonce( $_POST['security'], 'my_action_nonce' ) ) {
wp_die( __( 'Security check failed', 'wikit' ) );
}
// CORRECT: Complete form handler
public function handle_form_submission() {
// Check nonce
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'submit_form' ) ) {
wp_die( 'Security check failed' );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_die( 'Insufficient permissions' );
}
// Process form
$title = sanitize_text_field( $_POST['title'] );
// ... process data
}AJAX Nonces
php
// JAVASCRIPT: Send nonce with AJAX
jQuery.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'my_ajax_action',
nonce: MyAjax.nonce,
data: formData
}
});
// PHP: Verify AJAX nonce
add_action( 'wp_ajax_my_ajax_action', 'handle_ajax' );
function handle_ajax() {
// Verify nonce
check_ajax_referer( 'my_ajax_nonce', 'nonce' );
// Process request
$data = sanitize_text_field( $_POST['data'] );
wp_send_json_success( [ 'message' => 'Processed' ] );
}
// PHP: Localize nonce for JavaScript
wp_localize_script( 'my-script', 'MyAjax', [
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'my_ajax_nonce' ),
] );URL Nonces
php
// CREATING: Nonce in URL
$delete_url = wp_nonce_url(
admin_url( 'admin-post.php?action=delete&id=' . $id ),
'delete_item_' . $id
);
// VERIFYING: Check URL nonce
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'delete_item_' . $id ) ) {
wp_die( 'Invalid nonce' );
}Capability Checks
Always Check User Permissions
php
// CORRECT: Check capabilities before actions
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_die( __( 'You do not have permission to edit this post.', 'wikit' ) );
}
// CORRECT: Check multiple capabilities
if ( current_user_can( 'manage_options' ) ) {
// Admin only functionality
}
// CORRECT: Custom capability check
if ( current_user_can( 'edit_products' ) ) {
// Custom post type capability
}
// INCORRECT: Role checking
if ( current_user()->roles[0] === 'administrator' ) { // WRONG!
// Don't check roles directly
}Common Capabilities
| Capability | Description |
|---|---|
manage_options | Administrator level |
edit_posts | Edit own posts |
edit_others_posts | Edit any posts |
publish_posts | Publish posts |
upload_files | Upload media |
edit_theme_options | Customize theme |
activate_plugins | Manage plugins |
Database Security
Prepared Statements
php
// CORRECT: Use $wpdb->prepare()
global $wpdb;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts}
WHERE post_type = %s
AND post_status = %s
AND ID > %d",
$post_type,
'publish',
$min_id
)
);
// CORRECT: Insert with prepare
$wpdb->insert(
$wpdb->prefix . 'custom_table',
[
'user_id' => $user_id,
'meta_key' => $key,
'meta_value' => $value,
],
[ '%d', '%s', '%s' ]
);
// INCORRECT: Direct query
$results = $wpdb->get_results(
"SELECT * FROM {$wpdb->posts} WHERE ID = $id" // VULNERABLE!
);Data Format Specifiers
| Specifier | Type | Use Case |
|---|---|---|
%s | String | Text, titles |
%d | Integer | IDs, counts |
%f | Float | Prices, decimals |
File Upload Security
Validating Uploads
php
// CORRECT: Validate file uploads
function handle_file_upload() {
// Check nonce
if ( ! wp_verify_nonce( $_POST['upload_nonce'], 'file_upload' ) ) {
wp_die( 'Security check failed' );
}
// Check permissions
if ( ! current_user_can( 'upload_files' ) ) {
wp_die( 'Insufficient permissions' );
}
// Validate file type
$allowed_types = [ 'jpg', 'jpeg', 'png', 'pdf' ];
$file_info = wp_check_filetype( $_FILES['file']['name'], null );
if ( ! in_array( $file_info['ext'], $allowed_types, true ) ) {
wp_die( 'Invalid file type' );
}
// Use WordPress upload handler
$upload = wp_handle_upload(
$_FILES['file'],
[ 'test_form' => false ]
);
if ( isset( $upload['error'] ) ) {
wp_die( $upload['error'] );
}
return $upload;
}XSS Prevention
Preventing Cross-Site Scripting
php
// CORRECT: Escape JavaScript
<script>
var message = <?php echo wp_json_encode( $message ); ?>;
var userId = <?php echo absint( $user_id ); ?>;
</script>
// CORRECT: Escape data attributes
<div
data-config="<?php echo esc_attr( wp_json_encode( $config ) ); ?>"
data-id="<?php echo esc_attr( $id ); ?>"
>
// CORRECT: Inline event handlers (avoid if possible)
<button onclick="<?php echo esc_js( 'handleClick(' . absint( $id ) . ')' ); ?>">
Click
</button>
// BETTER: Use data attributes instead
<button class="action-button" data-id="<?php echo esc_attr( $id ); ?>">
Click
</button>CSRF Protection
Cross-Site Request Forgery Prevention
php
// CORRECT: REST API with nonce
register_rest_route( 'wdg/v1', '/update', [
'methods' => 'POST',
'callback' => 'handle_update',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
'args' => [
'nonce' => [
'required' => true,
'validate_callback' => function( $param ) {
return wp_verify_nonce( $param, 'wp_rest' );
},
],
],
] );SQL Injection Prevention
Safe Query Patterns
php
// CORRECT: Using WordPress APIs
$posts = get_posts( [
'post_type' => 'product',
'meta_key' => 'price',
'meta_value' => $price,
'meta_compare' => '>',
] );
// CORRECT: Safe meta queries
$args = [
'post_type' => 'product',
'meta_query' => [
[
'key' => sanitize_key( $_GET['meta_key'] ),
'value' => sanitize_text_field( $_GET['meta_value'] ),
'compare' => in_array( $_GET['compare'], [ '=', '!=', '>', '<' ], true )
? $_GET['compare']
: '=',
],
],
];
$query = new WP_Query( $args );Authentication & Sessions
Secure Authentication
php
// CORRECT: Check authentication
if ( ! is_user_logged_in() ) {
wp_redirect( wp_login_url() );
exit;
}
// CORRECT: Verify user
$user = wp_get_current_user();
if ( ! $user->exists() ) {
wp_die( 'Invalid user' );
}
// CORRECT: Password hashing (WordPress handles this)
$user_id = wp_create_user( $username, $password, $email );
// NEVER: Store plain passwords
update_user_meta( $user_id, 'password', $password ); // NEVER DO THIS!Security Headers
Implementing Security Headers
php
// Add to theme functions.php or plugin
add_action( 'send_headers', function() {
// Prevent clickjacking
header( 'X-Frame-Options: SAMEORIGIN' );
// XSS Protection
header( 'X-XSS-Protection: 1; mode=block' );
// Content Type Options
header( 'X-Content-Type-Options: nosniff' );
// Referrer Policy
header( 'Referrer-Policy: strict-origin-when-cross-origin' );
} );Common Security Mistakes
Never Do These
php
// NEVER: Use eval()
eval( $_POST['code'] ); // EXTREMELY DANGEROUS!
// NEVER: Use extract() on user input
extract( $_POST ); // Creates variables from user input!
// NEVER: Include files from user input
include $_GET['file'] . '.php'; // Path traversal vulnerability!
// NEVER: Execute shell commands with user input
exec( 'ls ' . $_GET['dir'] ); // Command injection!
// NEVER: Suppress errors in production
@file_get_contents( $url ); // Hides potential issues
// NEVER: Trust file extensions alone
if ( substr( $file, -4 ) === '.jpg' ) { // Insufficient validation!
// File could still be malicious
}
// NEVER: Use weak comparison for security
if ( $user_input == $secret ) { // Type juggling vulnerability!
// Use === instead
}Security Checklist
Before deploying any code, ensure:
- [ ] All user input is sanitized
- [ ] All output is escaped
- [ ] Nonces are used for forms and AJAX
- [ ] Capabilities are checked
- [ ] Database queries use prepare()
- [ ] File uploads are validated
- [ ] No use of eval(), extract(), or system()
- [ ] No suppressed errors (@)
- [ ] No hardcoded credentials
- [ ] HTTPS is enforced for sensitive operations
- [ ] Security headers are implemented
- [ ] Error messages don't expose sensitive info
- [ ] Logs don't contain sensitive data
Security Testing
Tools and Methods
WordPress Security Plugins
- Wordfence Security
- Sucuri Security
- iThemes Security
Code Analysis
- PHPCS with WordPress-Security ruleset
- RIPS Static Code Analysis
- SonarQube
Manual Testing
- Test with different user roles
- Try SQL injection in forms
- Attempt XSS in inputs
- Check for exposed files
Reporting Security Issues
If you discover a security vulnerability:
- Do NOT commit the vulnerable code
- Do NOT publicly disclose the issue
- Report to the security team immediately
- Document the issue privately
- Wait for a security patch before updating
Additional Resources
- WordPress Security Documentation
- OWASP Top 10
- WordPress Security White Paper
- WP VIP Security Best Practices
Remember: Security is not optional. Every line of code must be written with security in mind.