tigerstyle-life9/includes/class-security.php
Ryan Malloy e92b7f8700 Initial commit: TigerStyle Life9 v1.0.0
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.
2026-05-27 14:32:00 -06:00

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;
}
}