322 lines
9.4 KiB
PHP
322 lines
9.4 KiB
PHP
<?php
|
|
/**
|
|
* Security Handler Class
|
|
* Implements OWASP security best practices
|
|
*/
|
|
|
|
// Prevent direct access
|
|
if (!defined('WPINC')) {
|
|
die('Direct access not permitted.');
|
|
}
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class MCB_Security {
|
|
|
|
/**
|
|
* Initialize security features
|
|
*/
|
|
public static function init() {
|
|
// Add security headers
|
|
add_action('send_headers', array(__CLASS__, 'add_security_headers'));
|
|
|
|
// Add Content Security Policy
|
|
add_action('wp_head', array(__CLASS__, 'add_csp_meta'));
|
|
|
|
// Note: Global input sanitization removed - sanitize data at point of use instead
|
|
// This prevents conflicts with other plugins (e.g., WPForms)
|
|
}
|
|
|
|
/**
|
|
* Add security headers
|
|
*/
|
|
public static function add_security_headers() {
|
|
// Only add headers on pages with our plugin
|
|
if (!self::is_plugin_active_on_page()) {
|
|
return;
|
|
}
|
|
|
|
// X-Content-Type-Options
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
// X-Frame-Options
|
|
header('X-Frame-Options: SAMEORIGIN');
|
|
|
|
// X-XSS-Protection (legacy but still useful)
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
|
|
// Referrer Policy
|
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
}
|
|
|
|
/**
|
|
* Add Content Security Policy meta tag
|
|
*/
|
|
public static function add_csp_meta() {
|
|
if (!self::is_plugin_active_on_page()) {
|
|
return;
|
|
}
|
|
|
|
// Strict CSP for code viewer areas
|
|
$csp = "default-src 'self'; " .
|
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " . // Needed for Prism.js
|
|
"style-src 'self' 'unsafe-inline'; " . // Needed for inline styles
|
|
"img-src 'self' data: https:; " .
|
|
"connect-src 'self'; " .
|
|
"font-src 'self' data:; " .
|
|
"object-src 'none'; " .
|
|
"base-uri 'self'; " .
|
|
"form-action 'self'; " .
|
|
"frame-ancestors 'self';";
|
|
|
|
echo '<meta http-equiv="Content-Security-Policy" content="' . esc_attr($csp) . '">' . "\n";
|
|
}
|
|
|
|
/**
|
|
* Check if plugin is active on current page
|
|
*/
|
|
private static function is_plugin_active_on_page() {
|
|
global $post;
|
|
|
|
if (!is_a($post, 'WP_Post')) {
|
|
return false;
|
|
}
|
|
|
|
return has_shortcode($post->post_content, 'maple_code_block');
|
|
}
|
|
|
|
/**
|
|
* Sanitize global input arrays
|
|
*/
|
|
public static function sanitize_global_input() {
|
|
// Sanitize $_GET
|
|
if (!empty($_GET)) {
|
|
$_GET = self::sanitize_array($_GET);
|
|
}
|
|
|
|
// Sanitize $_POST
|
|
if (!empty($_POST)) {
|
|
$_POST = self::sanitize_array($_POST);
|
|
}
|
|
|
|
// Sanitize $_REQUEST
|
|
if (!empty($_REQUEST)) {
|
|
$_REQUEST = self::sanitize_array($_REQUEST);
|
|
}
|
|
|
|
// Sanitize $_COOKIE
|
|
if (!empty($_COOKIE)) {
|
|
$_COOKIE = self::sanitize_array($_COOKIE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively sanitize array
|
|
*/
|
|
private static function sanitize_array($array) {
|
|
foreach ($array as $key => $value) {
|
|
if (is_array($value)) {
|
|
$array[$key] = self::sanitize_array($value);
|
|
} else {
|
|
// Remove null bytes
|
|
$value = str_replace(chr(0), '', $value);
|
|
|
|
// Strip tags and encode special chars
|
|
$array[$key] = htmlspecialchars(strip_tags($value), ENT_QUOTES, 'UTF-8');
|
|
}
|
|
}
|
|
|
|
return $array;
|
|
}
|
|
|
|
/**
|
|
* Validate repository format
|
|
* Supports: owner/repo, platform:owner/repo, or full URLs
|
|
*/
|
|
public static function validate_repo_format($repo) {
|
|
// Remove any whitespace
|
|
$repo = trim($repo);
|
|
|
|
// If it's a full URL, validate it
|
|
if (strpos($repo, 'https://') === 0 || strpos($repo, 'http://') === 0) {
|
|
// Check if it's from a supported platform
|
|
$supported_domains = array(
|
|
'github.com',
|
|
'gitlab.com',
|
|
'bitbucket.org',
|
|
'codeberg.org'
|
|
);
|
|
|
|
$parsed = parse_url($repo);
|
|
if (!$parsed || !isset($parsed['host'])) {
|
|
return false;
|
|
}
|
|
|
|
$domain_valid = false;
|
|
foreach ($supported_domains as $domain) {
|
|
if (strpos($parsed['host'], $domain) !== false) {
|
|
$domain_valid = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$domain_valid) {
|
|
return false;
|
|
}
|
|
|
|
// Extract path and validate format
|
|
if (!isset($parsed['path'])) {
|
|
return false;
|
|
}
|
|
|
|
$path = trim($parsed['path'], '/');
|
|
$parts = explode('/', $path);
|
|
|
|
// Need at least owner/repo
|
|
if (count($parts) < 2) {
|
|
return false;
|
|
}
|
|
|
|
// Validate owner and repo names
|
|
$owner = $parts[0];
|
|
$repo_name = $parts[1];
|
|
|
|
if (!preg_match('/^[a-zA-Z0-9\-_]+$/', $owner) ||
|
|
!preg_match('/^[a-zA-Z0-9\-_\.]+$/', $repo_name)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Check for platform prefix (e.g., gitlab:owner/repo)
|
|
if (strpos($repo, ':') !== false) {
|
|
list($platform, $repo_path) = explode(':', $repo, 2);
|
|
|
|
// Validate platform
|
|
$valid_platforms = array('github', 'gitlab', 'bitbucket', 'codeberg');
|
|
if (!in_array($platform, $valid_platforms)) {
|
|
return false;
|
|
}
|
|
|
|
// Validate repo path
|
|
if (!preg_match('/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/', $repo_path)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Standard format: owner/repo
|
|
if (!preg_match('/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/', $repo)) {
|
|
return false;
|
|
}
|
|
|
|
// Check length limits
|
|
$parts = explode('/', $repo);
|
|
if (count($parts) !== 2) {
|
|
return false;
|
|
}
|
|
|
|
// Owner: 1-39 characters (GitHub limit)
|
|
if (strlen($parts[0]) < 1 || strlen($parts[0]) > 39) {
|
|
return false;
|
|
}
|
|
|
|
// Repo name: 1-100 characters (GitHub limit)
|
|
if (strlen($parts[1]) < 1 || strlen($parts[1]) > 100) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate file path
|
|
*/
|
|
public static function validate_file_path($path) {
|
|
// Remove leading/trailing slashes
|
|
$path = trim($path, '/');
|
|
|
|
// Check for path traversal attempts
|
|
$dangerous_patterns = array(
|
|
'..',
|
|
'//',
|
|
'\\',
|
|
'.git',
|
|
'.env',
|
|
'wp-config',
|
|
'.htaccess',
|
|
'.htpasswd'
|
|
);
|
|
|
|
foreach ($dangerous_patterns as $pattern) {
|
|
if (stripos($path, $pattern) !== false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Only allow safe characters
|
|
if (!preg_match('/^[a-zA-Z0-9\-_\.\/]+$/', $path)) {
|
|
return false;
|
|
}
|
|
|
|
// Check path depth (max 10 levels)
|
|
if (substr_count($path, '/') > 10) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Generate secure random token
|
|
*/
|
|
public static function generate_token($length = 32) {
|
|
if (function_exists('random_bytes')) {
|
|
return bin2hex(random_bytes($length));
|
|
} elseif (function_exists('openssl_random_pseudo_bytes')) {
|
|
return bin2hex(openssl_random_pseudo_bytes($length));
|
|
} else {
|
|
// Fallback to less secure method
|
|
return wp_generate_password($length * 2, false, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log security events
|
|
*/
|
|
public static function log_security_event($event_type, $details = array()) {
|
|
$log_entry = array(
|
|
'timestamp' => current_time('mysql'),
|
|
'event_type' => $event_type,
|
|
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
|
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
|
|
'details' => $details
|
|
);
|
|
|
|
// Store in WordPress transient for review (expires in 7 days)
|
|
$logs = get_transient('mcb_security_logs');
|
|
if (!is_array($logs)) {
|
|
$logs = array();
|
|
}
|
|
|
|
// Keep only last 100 entries
|
|
if (count($logs) >= 100) {
|
|
array_shift($logs);
|
|
}
|
|
|
|
$logs[] = $log_entry;
|
|
set_transient('mcb_security_logs', $logs, 7 * DAY_IN_SECONDS);
|
|
|
|
// For critical events, also log to error log
|
|
if (in_array($event_type, array('invalid_nonce', 'rate_limit_exceeded', 'path_traversal_attempt'))) {
|
|
error_log('GCV Security Event: ' . json_encode($log_entry));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize security features
|
|
MCB_Security::init();
|