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.