Because cats have 9 lives, but servers don't - so they need backup-restore! Complete backup solution with S3/MinIO support. - Full WordPress backup (files + database) - S3 / MinIO / S3-compatible storage backends - Scheduled automatic backups - Disaster recovery / one-click restore - Backup integrity validation - Cat-themed admin interface Includes build.sh and .distignore for WordPress-installable release ZIPs.
505 lines
14 KiB
PHP
505 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* TigerStyle Life9 Security Class
|
|
*
|
|
* Comprehensive security management for the backup plugin
|
|
* Implements security-first principles with cat-themed messaging
|
|
*
|
|
* @package TigerStyleLife9
|
|
* @subpackage Security
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
// Exit if accessed directly
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* TigerStyle Life9 Security Manager
|
|
*
|
|
* Handles all security aspects of the plugin including:
|
|
* - Input validation and sanitization
|
|
* - CSRF protection
|
|
* - Authentication and authorization
|
|
* - Security headers
|
|
* - Audit logging
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
class TigerStyle_Life9_Security {
|
|
|
|
/**
|
|
* Security instance
|
|
*
|
|
* @var TigerStyle_Life9_Security
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Security nonce action
|
|
*/
|
|
const NONCE_ACTION = 'tigerstyle_life9_security';
|
|
|
|
/**
|
|
* Security capabilities required
|
|
*/
|
|
const REQUIRED_CAPABILITY = 'manage_options';
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct() {
|
|
$this->init_hooks();
|
|
$this->setup_security_headers();
|
|
}
|
|
|
|
/**
|
|
* Get instance
|
|
*
|
|
* @return TigerStyle_Life9_Security
|
|
*/
|
|
public static function instance() {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Initialize security hooks
|
|
*/
|
|
private function init_hooks() {
|
|
// Security headers
|
|
add_action('send_headers', [$this, 'send_security_headers']);
|
|
|
|
// Admin security
|
|
add_action('admin_init', [$this, 'admin_security_check']);
|
|
|
|
// AJAX security
|
|
add_action('wp_ajax_tigerstyle_life9_*', [$this, 'verify_ajax_request'], 1);
|
|
|
|
// File upload security
|
|
add_filter('wp_handle_upload', [$this, 'secure_file_upload']);
|
|
|
|
// Content security
|
|
add_filter('wp_kses_allowed_html', [$this, 'filter_allowed_html'], 10, 2);
|
|
}
|
|
|
|
/**
|
|
* Setup security headers
|
|
*/
|
|
private function setup_security_headers() {
|
|
// Only apply to our plugin pages
|
|
if (!$this->is_plugin_page()) {
|
|
return;
|
|
}
|
|
|
|
// Content Security Policy
|
|
$csp = "default-src 'self'; ";
|
|
$csp .= "script-src 'self' 'unsafe-inline' 'unsafe-eval'; ";
|
|
$csp .= "style-src 'self' 'unsafe-inline'; ";
|
|
$csp .= "img-src 'self' data: blob:; ";
|
|
$csp .= "font-src 'self'; ";
|
|
$csp .= "connect-src 'self';";
|
|
|
|
header("Content-Security-Policy: $csp");
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('X-Frame-Options: DENY');
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
}
|
|
|
|
/**
|
|
* Send security headers
|
|
*/
|
|
public function send_security_headers() {
|
|
if ($this->is_plugin_page()) {
|
|
$this->setup_security_headers();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if current page is a plugin page
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function is_plugin_page() {
|
|
global $pagenow;
|
|
|
|
if (!is_admin()) {
|
|
return false;
|
|
}
|
|
|
|
// Check for our plugin pages
|
|
$plugin_pages = [
|
|
'admin.php?page=tigerstyle-life9',
|
|
'admin.php?page=tigerstyle-life9-backup',
|
|
'admin.php?page=tigerstyle-life9-restore',
|
|
'admin.php?page=tigerstyle-life9-settings'
|
|
];
|
|
|
|
$current_page = $pagenow . '?' . $_SERVER['QUERY_STRING'];
|
|
|
|
foreach ($plugin_pages as $page) {
|
|
if (strpos($current_page, $page) !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Verify CSRF token
|
|
*
|
|
* @param string $action Action to verify
|
|
* @param string $nonce Nonce to verify
|
|
* @return bool
|
|
*/
|
|
public function verify_nonce($action = null, $nonce = null) {
|
|
$action = $action ?: self::NONCE_ACTION;
|
|
$nonce = $nonce ?: $this->get_request_nonce();
|
|
|
|
if (!$nonce) {
|
|
return false;
|
|
}
|
|
|
|
return wp_verify_nonce($nonce, $action);
|
|
}
|
|
|
|
/**
|
|
* Create CSRF token
|
|
*
|
|
* @param string $action Action for the nonce
|
|
* @return string
|
|
*/
|
|
public function create_nonce($action = null) {
|
|
$action = $action ?: self::NONCE_ACTION;
|
|
return wp_create_nonce($action);
|
|
}
|
|
|
|
/**
|
|
* Get nonce from request
|
|
*
|
|
* @return string|null
|
|
*/
|
|
private function get_request_nonce() {
|
|
// Check various possible nonce locations
|
|
if (isset($_POST['_wpnonce'])) {
|
|
return sanitize_text_field($_POST['_wpnonce']);
|
|
}
|
|
|
|
if (isset($_GET['_wpnonce'])) {
|
|
return sanitize_text_field($_GET['_wpnonce']);
|
|
}
|
|
|
|
if (isset($_POST['tigerstyle_life9_nonce'])) {
|
|
return sanitize_text_field($_POST['tigerstyle_life9_nonce']);
|
|
}
|
|
|
|
// Check headers
|
|
$headers = getallheaders();
|
|
if (isset($headers['X-WP-Nonce'])) {
|
|
return sanitize_text_field($headers['X-WP-Nonce']);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Verify user capabilities
|
|
*
|
|
* @param string $capability Required capability
|
|
* @return bool
|
|
*/
|
|
public function verify_capability($capability = null) {
|
|
$capability = $capability ?: self::REQUIRED_CAPABILITY;
|
|
return current_user_can($capability);
|
|
}
|
|
|
|
/**
|
|
* Admin security check
|
|
*/
|
|
public function admin_security_check() {
|
|
if (!$this->is_plugin_page()) {
|
|
return;
|
|
}
|
|
|
|
// Verify user capability
|
|
if (!$this->verify_capability()) {
|
|
wp_die(__('🙀 Sorry! This territory is protected. You need proper cat credentials to access TigerStyle Life9 features.', 'tigerstyle-life9'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify AJAX request security
|
|
*/
|
|
public function verify_ajax_request() {
|
|
// Skip if not our AJAX call
|
|
if (strpos($_REQUEST['action'], 'tigerstyle_life9_') !== 0) {
|
|
return;
|
|
}
|
|
|
|
// Verify nonce
|
|
if (!$this->verify_nonce()) {
|
|
wp_send_json_error([
|
|
'message' => __('🙀 Security check failed! This cat is suspicious of your request.', 'tigerstyle-life9'),
|
|
'code' => 'invalid_nonce'
|
|
]);
|
|
}
|
|
|
|
// Verify capability
|
|
if (!$this->verify_capability()) {
|
|
wp_send_json_error([
|
|
'message' => __('🙀 Insufficient permissions! You need cat admin powers for this action.', 'tigerstyle-life9'),
|
|
'code' => 'insufficient_permissions'
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Secure file upload handler
|
|
*
|
|
* @param array $upload Upload data
|
|
* @return array
|
|
*/
|
|
public function secure_file_upload($upload) {
|
|
// Only process our uploads
|
|
if (!$this->is_plugin_upload()) {
|
|
return $upload;
|
|
}
|
|
|
|
$file_path = $upload['file'];
|
|
$file_type = $upload['type'];
|
|
|
|
// Validate file type
|
|
$allowed_types = ['application/zip', 'application/x-tar', 'application/gzip'];
|
|
if (!in_array($file_type, $allowed_types)) {
|
|
$upload['error'] = __('🙀 Invalid file type! This cat only accepts backup archives (.zip, .tar, .gz).', 'tigerstyle-life9');
|
|
return $upload;
|
|
}
|
|
|
|
// Validate file size (max 2GB)
|
|
$max_size = 2 * 1024 * 1024 * 1024; // 2GB
|
|
if (filesize($file_path) > $max_size) {
|
|
$upload['error'] = __('🙀 File too large! Even cats with 9 lives have storage limits.', 'tigerstyle-life9');
|
|
return $upload;
|
|
}
|
|
|
|
// Scan for malicious content
|
|
if ($this->scan_file_for_threats($file_path)) {
|
|
unlink($file_path);
|
|
$upload['error'] = __('🙀 Suspicious file detected! This cat\'s security instincts are tingling.', 'tigerstyle-life9');
|
|
return $upload;
|
|
}
|
|
|
|
return $upload;
|
|
}
|
|
|
|
/**
|
|
* Check if current upload is for our plugin
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function is_plugin_upload() {
|
|
return isset($_POST['tigerstyle_life9_upload']) ||
|
|
(isset($_GET['page']) && strpos($_GET['page'], 'tigerstyle-life9') === 0);
|
|
}
|
|
|
|
/**
|
|
* Scan file for security threats
|
|
*
|
|
* @param string $file_path Path to file
|
|
* @return bool True if threats found
|
|
*/
|
|
private function scan_file_for_threats($file_path) {
|
|
// Basic threat patterns
|
|
$threat_patterns = [
|
|
'/<\?php/', // PHP code
|
|
'/eval\s*\(/', // eval() calls
|
|
'/exec\s*\(/', // exec() calls
|
|
'/system\s*\(/', // system() calls
|
|
'/shell_exec\s*\(/', // shell_exec() calls
|
|
'/passthru\s*\(/', // passthru() calls
|
|
'/file_get_contents\s*\(/', // file_get_contents() calls
|
|
'/file_put_contents\s*\(/', // file_put_contents() calls
|
|
'/fopen\s*\(/', // fopen() calls
|
|
'/base64_decode\s*\(/', // base64_decode() calls
|
|
];
|
|
|
|
// Read first 1MB of file for scanning
|
|
$content = file_get_contents($file_path, false, null, 0, 1024 * 1024);
|
|
|
|
foreach ($threat_patterns as $pattern) {
|
|
if (preg_match($pattern, $content)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Filter allowed HTML for our plugin content
|
|
*
|
|
* @param array $allowed_html Allowed HTML tags
|
|
* @param string $context Context
|
|
* @return array
|
|
*/
|
|
public function filter_allowed_html($allowed_html, $context) {
|
|
if ($context !== 'tigerstyle_life9') {
|
|
return $allowed_html;
|
|
}
|
|
|
|
// Allow specific HTML for our plugin
|
|
$plugin_html = [
|
|
'div' => [
|
|
'class' => true,
|
|
'id' => true,
|
|
'data-*' => true
|
|
],
|
|
'span' => [
|
|
'class' => true,
|
|
'id' => true
|
|
],
|
|
'p' => [
|
|
'class' => true
|
|
],
|
|
'button' => [
|
|
'class' => true,
|
|
'type' => true,
|
|
'disabled' => true,
|
|
'data-*' => true
|
|
],
|
|
'input' => [
|
|
'type' => true,
|
|
'name' => true,
|
|
'value' => true,
|
|
'class' => true,
|
|
'disabled' => true,
|
|
'readonly' => true
|
|
],
|
|
'select' => [
|
|
'name' => true,
|
|
'class' => true,
|
|
'disabled' => true
|
|
],
|
|
'option' => [
|
|
'value' => true,
|
|
'selected' => true
|
|
],
|
|
'textarea' => [
|
|
'name' => true,
|
|
'class' => true,
|
|
'rows' => true,
|
|
'cols' => true,
|
|
'disabled' => true,
|
|
'readonly' => true
|
|
]
|
|
];
|
|
|
|
return array_merge($allowed_html, $plugin_html);
|
|
}
|
|
|
|
/**
|
|
* Sanitize array recursively
|
|
*
|
|
* @param array $data Data to sanitize
|
|
* @return array
|
|
*/
|
|
public function sanitize_array($data) {
|
|
if (!is_array($data)) {
|
|
return sanitize_text_field($data);
|
|
}
|
|
|
|
$sanitized = [];
|
|
foreach ($data as $key => $value) {
|
|
$key = sanitize_key($key);
|
|
$sanitized[$key] = is_array($value) ? $this->sanitize_array($value) : sanitize_text_field($value);
|
|
}
|
|
|
|
return $sanitized;
|
|
}
|
|
|
|
/**
|
|
* Log security event
|
|
*
|
|
* @param string $event Event type
|
|
* @param string $message Event message
|
|
* @param array $context Additional context
|
|
*/
|
|
public function log_security_event($event, $message, $context = []) {
|
|
if (!defined('WP_DEBUG') || !WP_DEBUG) {
|
|
return;
|
|
}
|
|
|
|
$log_entry = [
|
|
'timestamp' => current_time('mysql'),
|
|
'event' => $event,
|
|
'message' => $message,
|
|
'user_id' => get_current_user_id(),
|
|
'ip_address' => $this->get_client_ip(),
|
|
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
|
|
'context' => $context
|
|
];
|
|
|
|
error_log('TigerStyle Life9 Security: ' . wp_json_encode($log_entry));
|
|
}
|
|
|
|
/**
|
|
* Get client IP address
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_client_ip() {
|
|
$ip_headers = [
|
|
'HTTP_CF_CONNECTING_IP',
|
|
'HTTP_X_FORWARDED_FOR',
|
|
'HTTP_X_REAL_IP',
|
|
'REMOTE_ADDR'
|
|
];
|
|
|
|
foreach ($ip_headers as $header) {
|
|
if (isset($_SERVER[$header]) && !empty($_SERVER[$header])) {
|
|
$ip = sanitize_text_field($_SERVER[$header]);
|
|
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
|
return $ip;
|
|
}
|
|
}
|
|
}
|
|
|
|
return '0.0.0.0';
|
|
}
|
|
|
|
/**
|
|
* Rate limit check
|
|
*
|
|
* @param string $action Action being rate limited
|
|
* @param int $limit Maximum attempts
|
|
* @param int $window Time window in seconds
|
|
* @return bool True if rate limit exceeded
|
|
*/
|
|
public function check_rate_limit($action, $limit = 10, $window = 300) {
|
|
$ip = $this->get_client_ip();
|
|
$key = "tigerstyle_life9_rate_limit_{$action}_{$ip}";
|
|
|
|
$attempts = get_transient($key);
|
|
if ($attempts === false) {
|
|
set_transient($key, 1, $window);
|
|
return false;
|
|
}
|
|
|
|
if ($attempts >= $limit) {
|
|
$this->log_security_event('rate_limit_exceeded', "Rate limit exceeded for action: $action", [
|
|
'action' => $action,
|
|
'attempts' => $attempts,
|
|
'limit' => $limit
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
set_transient($key, $attempts + 1, $window);
|
|
return false;
|
|
}
|
|
} |