Skip to content

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

FunctionUse CaseExample
sanitize_text_field()Plain text fieldsTitles, names
sanitize_textarea_field()Multi-line plain textComments, descriptions
sanitize_email()Email addressesUser email
sanitize_file_name()File namesUpload names
sanitize_key()Database keysMeta keys, options
sanitize_title()URL slugsPost slugs
absint()Positive integersIDs, counts
intval()Any integerOffsets, limits
wp_kses_post()HTML contentPost content
esc_url_raw()URLs for storageDatabase 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

FunctionUse CaseContext
esc_html()HTML text content<p>text</p>
esc_attr()HTML attributesclass=""
esc_url()URLs in attributeshref=""
esc_js()Inline JavaScriptonclick=""
esc_textarea()Textarea content<textarea>
wp_kses()Custom HTML filteringRich content
wp_kses_post()Post content HTMLUser 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

CapabilityDescription
manage_optionsAdministrator level
edit_postsEdit own posts
edit_others_postsEdit any posts
publish_postsPublish posts
upload_filesUpload media
edit_theme_optionsCustomize theme
activate_pluginsManage 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

SpecifierTypeUse Case
%sStringText, titles
%dIntegerIDs, counts
%fFloatPrices, 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

  1. WordPress Security Plugins

    • Wordfence Security
    • Sucuri Security
    • iThemes Security
  2. Code Analysis

    • PHPCS with WordPress-Security ruleset
    • RIPS Static Code Analysis
    • SonarQube
  3. 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:

  1. Do NOT commit the vulnerable code
  2. Do NOT publicly disclose the issue
  3. Report to the security team immediately
  4. Document the issue privately
  5. Wait for a security patch before updating

Additional Resources

Remember: Security is not optional. Every line of code must be written with security in mind.

Released under the MIT License.