| 1 |
<?php |
| 2 |
/** |
| 3 |
* Security Headers Manager |
| 4 |
* |
| 5 |
* Manages security headers for the ThinkRank plugin to prevent |
| 6 |
* XSS, clickjacking, and other security vulnerabilities. |
| 7 |
* |
| 8 |
* @package ThinkRank |
| 9 |
* @subpackage Core |
| 10 |
* @since 1.0.0 |
| 11 |
*/ |
| 12 |
|
| 13 |
declare(strict_types=1); |
| 14 |
|
| 15 |
namespace ThinkRank\Core; |
| 16 |
|
| 17 |
// Prevent direct access |
| 18 |
if (!defined('ABSPATH')) { |
| 19 |
exit; |
| 20 |
} |
| 21 |
|
| 22 |
/** |
| 23 |
* Security Headers Manager Class |
| 24 |
* |
| 25 |
* Implements basic security headers for admin pages and API endpoints |
| 26 |
* to enhance security posture. |
| 27 |
* |
| 28 |
* @since 1.0.0 |
| 29 |
*/ |
| 30 |
class Security_Headers { |
| 31 |
|
| 32 |
/** |
| 33 |
* Initialize security headers |
| 34 |
* |
| 35 |
* @since 1.0.0 |
| 36 |
*/ |
| 37 |
public function __construct() { |
| 38 |
add_action('admin_init', [$this, 'add_admin_security_headers']); |
| 39 |
add_action('rest_api_init', [$this, 'add_api_security_headers']); |
| 40 |
} |
| 41 |
|
| 42 |
/** |
| 43 |
* Add security headers for admin pages |
| 44 |
* |
| 45 |
* @since 1.0.0 |
| 46 |
*/ |
| 47 |
public function add_admin_security_headers(): void { |
| 48 |
// Only add headers on ThinkRank admin pages |
| 49 |
if (!$this->is_thinkrank_admin_page()) { |
| 50 |
return; |
| 51 |
} |
| 52 |
|
| 53 |
// Prevent clickjacking |
| 54 |
if (!headers_sent()) { |
| 55 |
header('X-Frame-Options: SAMEORIGIN'); |
| 56 |
header('X-Content-Type-Options: nosniff'); |
| 57 |
header('X-XSS-Protection: 1; mode=block'); |
| 58 |
header('Referrer-Policy: strict-origin-when-cross-origin'); |
| 59 |
|
| 60 |
// Basic CSP for admin pages |
| 61 |
$csp = $this->get_admin_csp_policy(); |
| 62 |
header("Content-Security-Policy: {$csp}"); |
| 63 |
} |
| 64 |
} |
| 65 |
|
| 66 |
/** |
| 67 |
* Add security headers for API endpoints |
| 68 |
* |
| 69 |
* @since 1.0.0 |
| 70 |
*/ |
| 71 |
public function add_api_security_headers(): void { |
| 72 |
add_filter('rest_pre_serve_request', [$this, 'add_rest_security_headers'], 10, 4); |
| 73 |
} |
| 74 |
|
| 75 |
/** |
| 76 |
* Add security headers to REST API responses |
| 77 |
* |
| 78 |
* @since 1.0.0 |
| 79 |
* |
| 80 |
* @param bool $served Whether the request has already been served |
| 81 |
* @param WP_HTTP_Response $result Result to send to the client |
| 82 |
* @param WP_REST_Request $request Request used to generate the response |
| 83 |
* @param WP_REST_Server $server Server instance |
| 84 |
* @return bool |
| 85 |
*/ |
| 86 |
public function add_rest_security_headers($served, $result, $request, $server): bool { |
| 87 |
// Only add headers to ThinkRank API endpoints |
| 88 |
if (!$this->is_thinkrank_api_request($request)) { |
| 89 |
return $served; |
| 90 |
} |
| 91 |
|
| 92 |
if (!headers_sent()) { |
| 93 |
header('X-Content-Type-Options: nosniff'); |
| 94 |
header('X-Frame-Options: DENY'); |
| 95 |
header('X-XSS-Protection: 1; mode=block'); |
| 96 |
header('Referrer-Policy: strict-origin'); |
| 97 |
|
| 98 |
// CORS headers for API |
| 99 |
$this->add_cors_headers(); |
| 100 |
} |
| 101 |
|
| 102 |
return $served; |
| 103 |
} |
| 104 |
|
| 105 |
/** |
| 106 |
* Check if current page is a ThinkRank admin page |
| 107 |
* |
| 108 |
* @since 1.0.0 |
| 109 |
* |
| 110 |
* @return bool |
| 111 |
*/ |
| 112 |
private function is_thinkrank_admin_page(): bool { |
| 113 |
if (!is_admin()) { |
| 114 |
return false; |
| 115 |
} |
| 116 |
|
| 117 |
$screen = get_current_screen(); |
| 118 |
if (!$screen) { |
| 119 |
return false; |
| 120 |
} |
| 121 |
|
| 122 |
// Check if it's a ThinkRank admin page |
| 123 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading page parameter for screen detection, not processing form data. |
| 124 |
return strpos($screen->id, 'thinkrank') !== false || |
| 125 |
strpos($screen->base, 'thinkrank') !== false || |
| 126 |
(isset($_GET['page']) && strpos(sanitize_text_field(wp_unslash($_GET['page'])), 'thinkrank') !== false); // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 127 |
} |
| 128 |
|
| 129 |
/** |
| 130 |
* Check if request is to a ThinkRank API endpoint |
| 131 |
* |
| 132 |
* @since 1.0.0 |
| 133 |
* |
| 134 |
* @param WP_REST_Request $request Request object |
| 135 |
* @return bool |
| 136 |
*/ |
| 137 |
private function is_thinkrank_api_request($request): bool { |
| 138 |
$route = $request->get_route(); |
| 139 |
return strpos($route, '/thinkrank/') === 0; |
| 140 |
} |
| 141 |
|
| 142 |
/** |
| 143 |
* Get Content Security Policy for admin pages |
| 144 |
* |
| 145 |
* @since 1.0.0 |
| 146 |
* |
| 147 |
* @return string CSP policy string |
| 148 |
*/ |
| 149 |
private function get_admin_csp_policy(): string { |
| 150 |
$site_url = get_site_url(); |
| 151 |
$admin_url = admin_url(); |
| 152 |
|
| 153 |
$policies = [ |
| 154 |
"default-src 'self'", |
| 155 |
// 'unsafe-eval' dropped — the React admin bundle doesn't need it. We |
| 156 |
// still allow 'unsafe-inline' because WordPress core emits unnonced |
| 157 |
// inline admin scripts; moving the SPA to nonces/hashes is tracked |
| 158 |
// separately and can't be done without breaking core admin output. |
| 159 |
"script-src 'self' 'unsafe-inline' {$site_url} {$admin_url}", |
| 160 |
"style-src 'self' 'unsafe-inline' {$site_url} {$admin_url}", |
| 161 |
"img-src 'self' data: {$site_url}", |
| 162 |
"font-src 'self' {$site_url}", |
| 163 |
"connect-src 'self' {$site_url} https://api.openai.com https://api.anthropic.com https://generativelanguage.googleapis.com", |
| 164 |
"frame-src 'none'", |
| 165 |
"object-src 'none'", |
| 166 |
"base-uri 'self'" |
| 167 |
]; |
| 168 |
|
| 169 |
return implode('; ', $policies); |
| 170 |
} |
| 171 |
|
| 172 |
/** |
| 173 |
* Add CORS headers for API endpoints |
| 174 |
* |
| 175 |
* @since 1.0.0 |
| 176 |
*/ |
| 177 |
private function add_cors_headers(): void { |
| 178 |
$origin = get_site_url(); |
| 179 |
|
| 180 |
header("Access-Control-Allow-Origin: {$origin}"); |
| 181 |
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); |
| 182 |
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-WP-Nonce'); |
| 183 |
header('Access-Control-Allow-Credentials: true'); |
| 184 |
header('Access-Control-Max-Age: 86400'); // 24 hours |
| 185 |
} |
| 186 |
|
| 187 |
/** |
| 188 |
* Get security headers status |
| 189 |
* |
| 190 |
* @since 1.0.0 |
| 191 |
* |
| 192 |
* @return array Security headers status |
| 193 |
*/ |
| 194 |
public function get_security_status(): array { |
| 195 |
return [ |
| 196 |
'headers_enabled' => true, |
| 197 |
'csp_enabled' => true, |
| 198 |
'cors_configured' => true, |
| 199 |
'xss_protection' => true, |
| 200 |
'clickjacking_protection' => true, |
| 201 |
'content_type_protection' => true |
| 202 |
]; |
| 203 |
} |
| 204 |
} |
| 205 |
|