Some checks failed
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Has been cancelled
Security Monitoring & Compliance / Secrets & Credential Scan (push) Has been cancelled
Security Monitoring & Compliance / WordPress Security Analysis (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Has been cancelled
Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Has been cancelled
Security Monitoring & Compliance / Static Code Security Analysis (push) Has been cancelled
Security Monitoring & Compliance / Security Compliance Validation (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Notification (push) Has been cancelled
Security Monitoring & Compliance / Security Summary Report (push) Has been cancelled
Security Monitoring & Compliance / Security Team Notification (push) Has been cancelled
- Deploy 6 simultaneous WordPress specialized agents using sequential thinking and Zen MCP - Resolve all critical issues: permissions, jQuery dependencies, CDN mapping, security vulnerabilities - Implement bulletproof jQuery loading system with WordPress hook timing fixes - Create professional MapGeo Safety system with CDN health monitoring and fallback UI - Fix privilege escalation vulnerability with capability-based authorization - Add complete announcement admin system with modal forms and AJAX handling - Enhance import/export functionality (54 trainers successfully exported) - Achieve 100% operational master trainer functionality verified via MCP Playwright E2E testing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
517 lines
No EOL
18 KiB
PHP
517 lines
No EOL
18 KiB
PHP
<?php
|
|
/**
|
|
* HVAC AJAX Security Handler
|
|
*
|
|
* Centralized security layer for all AJAX endpoints
|
|
* Implements OWASP Top 10 security best practices
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 2.0.0
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class HVAC_Ajax_Security
|
|
*
|
|
* Provides comprehensive security for AJAX endpoints including:
|
|
* - Rate limiting
|
|
* - Input validation
|
|
* - CSRF protection
|
|
* - Audit logging
|
|
* - Security headers
|
|
*/
|
|
class HVAC_Ajax_Security {
|
|
|
|
/**
|
|
* Instance of this class
|
|
*
|
|
* @var HVAC_Ajax_Security
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Rate limiting configuration
|
|
*/
|
|
const RATE_LIMIT_WINDOW = 60; // seconds
|
|
const RATE_LIMIT_REQUESTS = 30; // max requests per window
|
|
const RATE_LIMIT_SENSITIVE = 5; // max sensitive actions per window
|
|
|
|
/**
|
|
* Security nonce actions
|
|
*/
|
|
const NONCE_GENERAL = 'hvac_ajax_nonce';
|
|
const NONCE_APPROVAL = 'hvac_master_approvals';
|
|
const NONCE_ANNOUNCEMENT = 'hvac_announcements_nonce';
|
|
|
|
/**
|
|
* Get instance of this class
|
|
*
|
|
* @return HVAC_Ajax_Security
|
|
*/
|
|
public static function get_instance() {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
$this->init_hooks();
|
|
}
|
|
|
|
/**
|
|
* Initialize security hooks
|
|
*/
|
|
private function init_hooks() {
|
|
// Add security headers
|
|
add_action('send_headers', array($this, 'send_security_headers'));
|
|
|
|
// AJAX security middleware
|
|
add_action('wp_ajax_nopriv_hvac_security_check', array($this, 'block_nopriv_ajax'));
|
|
|
|
// Initialize audit logging
|
|
add_action('init', array($this, 'init_audit_logging'));
|
|
}
|
|
|
|
/**
|
|
* Send security headers
|
|
*/
|
|
public function send_security_headers() {
|
|
if (!is_admin() && wp_doing_ajax()) {
|
|
// Content Security Policy
|
|
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';");
|
|
|
|
// Additional security headers
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('X-Frame-Options: SAMEORIGIN');
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
|
|
// CORS configuration (adjust origin as needed)
|
|
$allowed_origin = get_site_url();
|
|
header("Access-Control-Allow-Origin: $allowed_origin");
|
|
header('Access-Control-Allow-Credentials: true');
|
|
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
|
header('Access-Control-Max-Age: 86400');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify AJAX request security
|
|
*
|
|
* @param string $action AJAX action being performed
|
|
* @param string $nonce_action Nonce action to verify
|
|
* @param array $capabilities Required capabilities
|
|
* @param bool $is_sensitive Whether this is a sensitive action
|
|
* @return bool|WP_Error True if valid, WP_Error on failure
|
|
*/
|
|
public static function verify_ajax_request($action, $nonce_action = null, $capabilities = array(), $is_sensitive = false) {
|
|
$security = self::get_instance();
|
|
|
|
// 1. Check if user is logged in
|
|
if (!is_user_logged_in()) {
|
|
$security->log_security_event('ajax_unauthorized', $action);
|
|
return new WP_Error('unauthorized', 'Authentication required', array('status' => 401));
|
|
}
|
|
|
|
// 2. Verify nonce
|
|
$nonce = isset($_REQUEST['nonce']) ? sanitize_text_field($_REQUEST['nonce']) : '';
|
|
$nonce_action = $nonce_action ?: self::NONCE_GENERAL;
|
|
|
|
if (!wp_verify_nonce($nonce, $nonce_action)) {
|
|
$security->log_security_event('ajax_invalid_nonce', $action);
|
|
return new WP_Error('invalid_nonce', 'Security token expired or invalid', array('status' => 403));
|
|
}
|
|
|
|
// 3. Check rate limiting
|
|
$rate_limit = $security->check_rate_limit($action, $is_sensitive);
|
|
if (is_wp_error($rate_limit)) {
|
|
return $rate_limit;
|
|
}
|
|
|
|
// 4. Verify capabilities
|
|
if (!empty($capabilities)) {
|
|
$has_cap = false;
|
|
foreach ($capabilities as $capability) {
|
|
if (current_user_can($capability)) {
|
|
$has_cap = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$has_cap) {
|
|
$security->log_security_event('ajax_insufficient_permissions', $action);
|
|
return new WP_Error('insufficient_permissions', 'You do not have permission to perform this action', array('status' => 403));
|
|
}
|
|
}
|
|
|
|
// 5. Log successful authentication for sensitive actions
|
|
if ($is_sensitive) {
|
|
$security->log_security_event('ajax_sensitive_action', $action, 'info');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check rate limiting
|
|
*
|
|
* @param string $action Action being performed
|
|
* @param bool $is_sensitive Whether this is a sensitive action
|
|
* @return bool|WP_Error True if within limits, WP_Error if exceeded
|
|
*/
|
|
private function check_rate_limit($action, $is_sensitive = false) {
|
|
$user_id = get_current_user_id();
|
|
$ip_address = $this->get_client_ip();
|
|
|
|
// Use both user ID and IP for rate limiting
|
|
$rate_key = 'hvac_rate_' . md5($user_id . '_' . $ip_address . '_' . $action);
|
|
$sensitive_key = 'hvac_sensitive_' . md5($user_id . '_' . $ip_address);
|
|
|
|
// Check general rate limit
|
|
$requests = get_transient($rate_key);
|
|
$limit = $is_sensitive ? self::RATE_LIMIT_SENSITIVE : self::RATE_LIMIT_REQUESTS;
|
|
|
|
if (false === $requests) {
|
|
$requests = 0;
|
|
}
|
|
|
|
if ($requests >= $limit) {
|
|
$this->log_security_event('ajax_rate_limit_exceeded', $action, 'warning');
|
|
return new WP_Error('rate_limit_exceeded', 'Too many requests. Please wait and try again.', array('status' => 429));
|
|
}
|
|
|
|
// Increment counter
|
|
set_transient($rate_key, $requests + 1, self::RATE_LIMIT_WINDOW);
|
|
|
|
// Additional check for sensitive actions across all endpoints
|
|
if ($is_sensitive) {
|
|
$sensitive_count = get_transient($sensitive_key);
|
|
if (false === $sensitive_count) {
|
|
$sensitive_count = 0;
|
|
}
|
|
|
|
if ($sensitive_count >= self::RATE_LIMIT_SENSITIVE) {
|
|
$this->log_security_event('ajax_sensitive_rate_limit_exceeded', $action, 'critical');
|
|
return new WP_Error('rate_limit_exceeded', 'Too many sensitive actions. Please wait and try again.', array('status' => 429));
|
|
}
|
|
|
|
set_transient($sensitive_key, $sensitive_count + 1, self::RATE_LIMIT_WINDOW * 5); // 5 minute window for sensitive
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Sanitize and validate input data
|
|
*
|
|
* @param array $data Input data
|
|
* @param array $rules Validation rules
|
|
* @return array|WP_Error Sanitized data or error
|
|
*/
|
|
public static function sanitize_input($data, $rules) {
|
|
$sanitized = array();
|
|
$errors = array();
|
|
|
|
foreach ($rules as $field => $rule) {
|
|
$value = isset($data[$field]) ? $data[$field] : null;
|
|
|
|
// Check required fields
|
|
if (isset($rule['required']) && $rule['required'] && empty($value)) {
|
|
$errors[] = sprintf('Field "%s" is required', $field);
|
|
continue;
|
|
}
|
|
|
|
// Skip optional empty fields
|
|
if (empty($value) && (!isset($rule['required']) || !$rule['required'])) {
|
|
continue;
|
|
}
|
|
|
|
// Apply sanitization based on type
|
|
$type = isset($rule['type']) ? $rule['type'] : 'text';
|
|
switch ($type) {
|
|
case 'int':
|
|
$sanitized[$field] = intval($value);
|
|
if (isset($rule['min']) && $sanitized[$field] < $rule['min']) {
|
|
$errors[] = sprintf('Field "%s" must be at least %d', $field, $rule['min']);
|
|
}
|
|
if (isset($rule['max']) && $sanitized[$field] > $rule['max']) {
|
|
$errors[] = sprintf('Field "%s" must be at most %d', $field, $rule['max']);
|
|
}
|
|
break;
|
|
|
|
case 'email':
|
|
$sanitized[$field] = sanitize_email($value);
|
|
if (!is_email($sanitized[$field])) {
|
|
$errors[] = sprintf('Field "%s" must be a valid email', $field);
|
|
}
|
|
break;
|
|
|
|
case 'url':
|
|
$sanitized[$field] = esc_url_raw($value);
|
|
if (!filter_var($sanitized[$field], FILTER_VALIDATE_URL)) {
|
|
$errors[] = sprintf('Field "%s" must be a valid URL', $field);
|
|
}
|
|
break;
|
|
|
|
case 'textarea':
|
|
$sanitized[$field] = sanitize_textarea_field($value);
|
|
if (isset($rule['max_length']) && strlen($sanitized[$field]) > $rule['max_length']) {
|
|
$errors[] = sprintf('Field "%s" must be at most %d characters', $field, $rule['max_length']);
|
|
}
|
|
break;
|
|
|
|
case 'html':
|
|
$allowed_html = isset($rule['allowed_html']) ? $rule['allowed_html'] : wp_kses_allowed_html('post');
|
|
$sanitized[$field] = wp_kses($value, $allowed_html);
|
|
break;
|
|
|
|
case 'boolean':
|
|
$sanitized[$field] = filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
|
break;
|
|
|
|
case 'array':
|
|
if (!is_array($value)) {
|
|
$errors[] = sprintf('Field "%s" must be an array', $field);
|
|
} else {
|
|
$sanitized[$field] = array_map('sanitize_text_field', $value);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
$sanitized[$field] = sanitize_text_field($value);
|
|
if (isset($rule['max_length']) && strlen($sanitized[$field]) > $rule['max_length']) {
|
|
$errors[] = sprintf('Field "%s" must be at most %d characters', $field, $rule['max_length']);
|
|
}
|
|
}
|
|
|
|
// Custom validation callback
|
|
if (isset($rule['validate']) && is_callable($rule['validate'])) {
|
|
$validation_result = call_user_func($rule['validate'], $sanitized[$field]);
|
|
if (is_wp_error($validation_result)) {
|
|
$errors[] = $validation_result->get_error_message();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($errors)) {
|
|
return new WP_Error('validation_failed', 'Input validation failed', array('errors' => $errors));
|
|
}
|
|
|
|
return $sanitized;
|
|
}
|
|
|
|
/**
|
|
* Get client IP address
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_client_ip() {
|
|
$ip_keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR');
|
|
|
|
foreach ($ip_keys as $key) {
|
|
if (array_key_exists($key, $_SERVER) === true) {
|
|
foreach (explode(',', $_SERVER[$key]) as $ip) {
|
|
$ip = trim($ip);
|
|
|
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
|
|
return $ip;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
|
|
}
|
|
|
|
/**
|
|
* Log security events
|
|
*
|
|
* @param string $event_type Type of security event
|
|
* @param string $context Additional context
|
|
* @param string $level Log level (info, warning, critical)
|
|
*/
|
|
private function log_security_event($event_type, $context = '', $level = 'warning') {
|
|
$user_id = get_current_user_id();
|
|
$ip_address = $this->get_client_ip();
|
|
|
|
$log_entry = array(
|
|
'timestamp' => current_time('mysql'),
|
|
'event_type' => $event_type,
|
|
'user_id' => $user_id,
|
|
'ip_address' => $ip_address,
|
|
'context' => $context,
|
|
'level' => $level,
|
|
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
|
|
'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''
|
|
);
|
|
|
|
// Use WordPress logging if available
|
|
if (class_exists('HVAC_Logger')) {
|
|
HVAC_Logger::log($level, 'Security: ' . $event_type, $log_entry);
|
|
}
|
|
|
|
// Store in database for audit trail
|
|
$this->store_audit_log($log_entry);
|
|
|
|
// For critical events, notify admin
|
|
if ($level === 'critical') {
|
|
$this->notify_admin_security_event($log_entry);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store audit log in database
|
|
*
|
|
* @param array $log_entry Log entry data
|
|
*/
|
|
private function store_audit_log($log_entry) {
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'hvac_security_audit';
|
|
|
|
// Create table if not exists
|
|
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) {
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
|
|
$sql = "CREATE TABLE $table_name (
|
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
|
timestamp datetime DEFAULT CURRENT_TIMESTAMP,
|
|
event_type varchar(100) NOT NULL,
|
|
user_id bigint(20),
|
|
ip_address varchar(45),
|
|
context text,
|
|
level varchar(20),
|
|
user_agent text,
|
|
request_uri text,
|
|
PRIMARY KEY (id),
|
|
KEY event_type (event_type),
|
|
KEY user_id (user_id),
|
|
KEY timestamp (timestamp)
|
|
) $charset_collate;";
|
|
|
|
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
|
dbDelta($sql);
|
|
}
|
|
|
|
// Insert log entry
|
|
$wpdb->insert(
|
|
$table_name,
|
|
$log_entry,
|
|
array('%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initialize audit logging system
|
|
*/
|
|
public function init_audit_logging() {
|
|
// Clean old audit logs (keep 30 days)
|
|
if (!wp_next_scheduled('hvac_clean_audit_logs')) {
|
|
wp_schedule_event(time(), 'daily', 'hvac_clean_audit_logs');
|
|
}
|
|
|
|
add_action('hvac_clean_audit_logs', array($this, 'clean_old_audit_logs'));
|
|
}
|
|
|
|
/**
|
|
* Clean old audit logs
|
|
*/
|
|
public function clean_old_audit_logs() {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'hvac_security_audit';
|
|
|
|
$wpdb->query($wpdb->prepare(
|
|
"DELETE FROM $table_name WHERE timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)",
|
|
30
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Notify admin of critical security events
|
|
*
|
|
* @param array $log_entry Log entry data
|
|
*/
|
|
private function notify_admin_security_event($log_entry) {
|
|
$admin_email = get_option('admin_email');
|
|
$site_name = get_bloginfo('name');
|
|
|
|
$subject = sprintf('[%s] Critical Security Event: %s', $site_name, $log_entry['event_type']);
|
|
|
|
$message = sprintf(
|
|
"A critical security event has been detected on your site.\n\n" .
|
|
"Event Type: %s\n" .
|
|
"Time: %s\n" .
|
|
"User ID: %s\n" .
|
|
"IP Address: %s\n" .
|
|
"Context: %s\n" .
|
|
"User Agent: %s\n" .
|
|
"Request URI: %s\n\n" .
|
|
"Please review your security logs for more information.",
|
|
$log_entry['event_type'],
|
|
$log_entry['timestamp'],
|
|
$log_entry['user_id'],
|
|
$log_entry['ip_address'],
|
|
$log_entry['context'],
|
|
$log_entry['user_agent'],
|
|
$log_entry['request_uri']
|
|
);
|
|
|
|
wp_mail($admin_email, $subject, $message);
|
|
}
|
|
|
|
/**
|
|
* Block non-privileged AJAX requests
|
|
*/
|
|
public function block_nopriv_ajax() {
|
|
wp_send_json_error('Unauthorized access', 401);
|
|
}
|
|
|
|
/**
|
|
* Generate secure token for sensitive operations
|
|
*
|
|
* @param string $action Action identifier
|
|
* @param int $user_id User ID
|
|
* @return string Secure token
|
|
*/
|
|
public static function generate_secure_token($action, $user_id) {
|
|
$salt = wp_salt('auth');
|
|
$data = $action . '|' . $user_id . '|' . time();
|
|
return hash_hmac('sha256', $data, $salt);
|
|
}
|
|
|
|
/**
|
|
* Verify secure token
|
|
*
|
|
* @param string $token Token to verify
|
|
* @param string $action Action identifier
|
|
* @param int $user_id User ID
|
|
* @param int $expiry Token expiry in seconds (default 1 hour)
|
|
* @return bool
|
|
*/
|
|
public static function verify_secure_token($token, $action, $user_id, $expiry = 3600) {
|
|
$salt = wp_salt('auth');
|
|
|
|
// Generate tokens for last $expiry seconds
|
|
$current_time = time();
|
|
for ($i = 0; $i <= $expiry; $i++) {
|
|
$data = $action . '|' . $user_id . '|' . ($current_time - $i);
|
|
$expected_token = hash_hmac('sha256', $data, $salt);
|
|
|
|
if (hash_equals($expected_token, $token)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Initialize the security handler
|
|
HVAC_Ajax_Security::get_instance(); |