tigerstyle-scent/includes/modules/class-scent-server.php
Ryan Malloy 120f0b616d Add release tooling and update for v1.0.0 release
- Add .distignore (operator-private files excluded)
- Add build.sh for WordPress-installable release ZIPs
- Update CLAUDE.md references (now operator-private only)
2026-05-27 14:32:07 -06:00

987 lines
36 KiB
PHP

<?php
/**
* TigerStyle Scent Server Implementation
*
* Implements secure OAuth2 authentication like a cat's scent recognition system
* with support for territory code flow and scent validation
*
* @package TigerStyle Scent
*/
defined('ABSPATH') or die('Direct access forbidden.');
class TigerStyleScent_ScentServer {
/**
* WordPress database instance
* @var \wpdb
*/
private \wpdb $wpdb;
/**
* Plugin settings
* @var array
*/
private array $settings;
/**
* Constructor
*
* @param array $settings Plugin configuration settings
*/
public function __construct(array $settings) {
global $wpdb;
$this->wpdb = $wpdb;
$this->settings = $settings;
}
/**
* Handle scent authentication requests (OAuth2 endpoints)
*/
public function handle_scent_request(): void {
$endpoint = get_query_var('oauth_endpoint');
// 🔐 SECURITY: Enforce HTTPS for all OAuth2 endpoints
$this->enforce_https();
// 🔐 SECURITY: Add comprehensive security headers
$this->add_security_headers();
// 🔐 SECURITY: Check for blocked IPs and emergency blocks
if ($this->is_client_blocked()) {
$this->send_blocked_response();
return;
}
// 🔐 SECURITY: Validate request integrity
if (!$this->validate_request_integrity()) {
$this->log_security_violation('Request integrity validation failed');
$this->send_error_response(400, 'invalid_request', 'Request validation failed');
return;
}
// 🔐 SECURITY: Check rate limits before processing request
if (!TigerStyleScent_RateLimiter::check_rate_limit($endpoint)) {
TigerStyleScent_RateLimiter::send_rate_limit_response($endpoint);
return;
}
switch ($endpoint) {
case 'authorize':
$this->handle_territory_authorization();
break;
case 'token':
$this->handle_scent_token_exchange();
break;
case 'introspect':
$this->handle_scent_analysis();
break;
case 'revoke':
$this->handle_scent_revocation();
break;
default:
$this->send_error_response(404, 'unknown_territory', 'Unknown territory endpoint');
}
}
/**
* Handle territory authorization (OAuth2 authorize endpoint)
*/
private function handle_territory_authorization(): void {
// 🔐 SECURITY: Comprehensive OAuth2 parameter validation
$validation_rules = TigerStyleScent_InputValidator::get_oauth2_validation_rules();
$auth_rules = [
'response_type' => $validation_rules['response_type'],
'client_id' => $validation_rules['client_id'],
'redirect_uri' => $validation_rules['redirect_uri'],
'scope' => $validation_rules['scope'],
'state' => $validation_rules['state']
];
$validation_result = TigerStyleScent_InputValidator::validate_oauth2_request($_GET, $auth_rules);
if (!$validation_result['valid']) {
// Log validation failure with detailed context
TigerStyleScent_SecurityLogger::log_security_event(
TigerStyleScent_SecurityLogger::EVENT_VALIDATION_FAILURE,
TigerStyleScent_SecurityLogger::SEVERITY_MEDIUM,
'Authorization endpoint validation failed',
[
'errors' => $validation_result['errors'],
'warnings' => $validation_result['warnings'],
'raw_input' => $_GET
]
);
$this->send_error_response(400, 'invalid_request', 'Invalid request parameters');
return;
}
// Extract validated parameters
$response_type = $validation_result['sanitized']['response_type'];
$client_id = $validation_result['sanitized']['client_id'];
$redirect_uri = $validation_result['sanitized']['redirect_uri'];
$scope = $validation_result['sanitized']['scope'] ?? 'basic';
$state = $validation_result['sanitized']['state'] ?? '';
// Validate required parameters for territory access
if (empty($response_type) || empty($client_id)) {
$this->send_error_response(400, 'invalid_request', 'Missing required territory parameters');
return;
}
// Only support authorization code (territory code) flow
if ($response_type !== 'code') {
$this->send_error_response(400, 'unsupported_response_type', 'Only territory code flow is supported');
return;
}
// Validate client scent credentials
$client = $this->recognize_client_scent($client_id);
if (!$client) {
$this->send_error_response(400, 'invalid_client', 'Unknown client scent');
return;
}
// Validate redirect URI for safe territory return
if (!empty($redirect_uri) && !$this->validate_territory_return_uri($client, $redirect_uri)) {
$this->send_error_response(400, 'invalid_request', 'Invalid territory return URI');
return;
}
// Check if user is authenticated (has proper scent)
if (!is_user_logged_in()) {
// Redirect to WordPress login with return URL
$login_url = wp_login_url(add_query_arg($_GET, admin_url('admin.php')));
wp_redirect($login_url);
exit;
}
// Handle POST request (user authorization decision)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['authorize']) && wp_verify_nonce($_POST['_wpnonce'], 'tigerstyle_scent_authorize')) {
$this->grant_territory_access($client_id, $redirect_uri, $scope, $state);
} elseif (isset($_POST['deny'])) {
$this->deny_territory_access($redirect_uri, $state);
}
return;
}
// Show territory authorization form
$this->show_territory_authorization_form([
'client' => $client,
'scope' => $scope,
'state' => $state,
'redirect_uri' => $redirect_uri,
'client_id' => $client_id
]);
}
/**
* Handle scent token exchange (OAuth2 token endpoint)
*/
private function handle_scent_token_exchange(): void {
// Only accept POST requests for scent token exchange
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->send_error_response(405, 'method_not_allowed', 'Scent exchange requires POST');
return;
}
$grant_type = sanitize_text_field($_POST['grant_type'] ?? '');
switch ($grant_type) {
case 'authorization_code':
$this->handle_territory_code_grant();
break;
case 'refresh_token':
$this->handle_scent_refresh_grant();
break;
case 'client_credentials':
$this->handle_client_scent_credentials_grant();
break;
default:
$this->send_error_response(400, 'unsupported_grant_type', 'Unsupported scent grant type');
}
}
/**
* Handle territory code grant (authorization code flow)
*/
private function handle_territory_code_grant(): void {
// Extract scent parameters
$code = sanitize_text_field($_POST['code'] ?? '');
$client_id = sanitize_text_field($_POST['client_id'] ?? '');
$redirect_uri = esc_url_raw($_POST['redirect_uri'] ?? '');
$code_verifier = sanitize_text_field($_POST['code_verifier'] ?? '');
// Get client scent credentials from Authorization header or POST
$client_credentials = $this->extract_client_scent_credentials();
if ($client_credentials) {
$client_id = $client_credentials['client_id'];
$client_secret = $client_credentials['client_secret'];
} else {
$client_secret = sanitize_text_field($_POST['client_secret'] ?? '');
}
// Validate required scent parameters
if (empty($code) || empty($client_id)) {
$this->send_error_response(400, 'invalid_request', 'Missing required scent parameters');
return;
}
// Get and validate territory code
$territory_code = $this->get_territory_code($code);
if (!$territory_code) {
$this->send_error_response(400, 'invalid_grant', 'Invalid territory code');
return;
}
// Check if territory code has expired
if (strtotime($territory_code['expires']) < time()) {
$this->delete_territory_code($code);
$this->send_error_response(400, 'invalid_grant', 'Territory code expired');
return;
}
// Validate client scent match
if ($territory_code['client_id'] !== $client_id) {
$this->send_error_response(400, 'invalid_client', 'Client scent mismatch');
return;
}
// Get client details for scent recognition
$client = $this->recognize_client_scent($client_id);
if (!$client) {
// 🔐 SECURITY: Log client authentication failure
TigerStyleScent_SecurityLogger::log_security_event(
TigerStyleScent_SecurityLogger::EVENT_AUTH_FAILURE,
TigerStyleScent_SecurityLogger::SEVERITY_MEDIUM,
'Unknown client attempted authentication',
['client_id' => $client_id, 'endpoint' => 'token'],
$client_id
);
$this->send_error_response(400, 'invalid_client', 'Invalid client scent');
return;
}
// Validate client secret for confidential clients (strong scent verification)
if (!$client['is_public']) {
if (empty($client_secret) || !password_verify($client_secret, $client['client_secret'])) {
// 🔐 SECURITY: Log client secret failure
TigerStyleScent_SecurityLogger::log_security_event(
TigerStyleScent_SecurityLogger::EVENT_AUTH_FAILURE,
TigerStyleScent_SecurityLogger::SEVERITY_HIGH,
'Invalid client secret provided',
['client_id' => $client_id, 'endpoint' => 'token', 'is_public' => false],
$client_id
);
$this->send_error_response(401, 'invalid_client', 'Invalid scent credentials');
return;
}
}
// Validate territory return URI
if (!empty($redirect_uri) && $territory_code['redirect_uri'] !== $redirect_uri) {
$this->send_error_response(400, 'invalid_grant', 'Territory return URI mismatch');
return;
}
// Generate scent tokens
$scent_token = $this->generate_scent_token($client_id, $territory_code['user_id'], $territory_code['scope']);
$refresh_scent = $this->generate_refresh_scent($client_id, $territory_code['user_id'], $territory_code['scope']);
// 🔐 SECURITY: Log successful token issuance
TigerStyleScent_SecurityLogger::log_security_event(
TigerStyleScent_SecurityLogger::EVENT_TOKEN_ISSUED,
TigerStyleScent_SecurityLogger::SEVERITY_LOW,
'OAuth2 tokens successfully issued',
[
'grant_type' => 'authorization_code',
'scope' => $territory_code['scope'],
'token_length' => strlen($scent_token),
'refresh_token_issued' => !empty($refresh_scent)
],
$client_id,
$territory_code['user_id']
);
// Delete territory code (one-time use like a scent trail)
$this->delete_territory_code($code);
// Send scent token response
$this->send_scent_token_response($scent_token, $refresh_scent, $territory_code['scope']);
}
/**
* Handle scent analysis (OAuth2 introspect endpoint)
*/
private function handle_scent_analysis(): void {
// 🔐 CSRF Protection for POST requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nonce = sanitize_text_field($_POST['_wpnonce'] ?? '');
if (!wp_verify_nonce($nonce, 'tigerstyle_scent_introspect')) {
$this->send_error_response(403, 'invalid_request', 'CSRF token required');
return;
}
}
$token = sanitize_text_field($_POST['token'] ?? '');
if (empty($token)) {
$this->send_error_response(400, 'invalid_request', 'Missing scent token for analysis');
return;
}
$scent_data = $this->analyze_scent_token($token);
header('Content-Type: application/json');
echo json_encode($scent_data);
exit;
}
/**
* Recognize client by their unique scent (get client data)
*/
private function recognize_client_scent(string $client_id): ?array {
// Query WordPress posts to find OAuth2 client by scent signature
$posts = get_posts([
'post_type' => 'oauth2_client',
'meta_key' => 'client_id',
'meta_value' => $client_id,
'posts_per_page' => 1,
'post_status' => 'publish'
]);
if (empty($posts)) {
return null;
}
$post = $posts[0];
$client_scent_data = [
'client_id' => get_post_meta($post->ID, 'client_id', true),
'client_secret' => get_post_meta($post->ID, 'client_secret', true),
'redirect_uris' => get_post_meta($post->ID, 'redirect_uris', true),
'grant_types' => get_post_meta($post->ID, 'grant_types', true),
'scope' => get_post_meta($post->ID, 'scope', true),
'is_public' => (bool)get_post_meta($post->ID, 'is_public', true),
'name' => $post->post_title,
'client_name' => $post->post_title // Add client_name for scent recognition display
];
return $client_scent_data;
}
/**
* Validate territory return URI for safe scent trail
*/
private function validate_territory_return_uri(array $client, string $redirect_uri): bool {
$redirect_uris = $client['redirect_uris'] ?? '';
if (empty($redirect_uris)) {
return false;
}
$allowed_uris = explode(',', $redirect_uris);
return in_array($redirect_uri, array_map('trim', $allowed_uris));
}
/**
* Generate unique scent token (access token)
*/
private function generate_scent_token(string $client_id, int $user_id, string $scope): string {
// 🔐 SECURITY: Maximum entropy token generation (48 bytes = 384 bits)
$scent_token = $this->generate_secure_token(48);
$expires = date('Y-m-d H:i:s', time() + 3600); // 1 hour scent trail
// Store scent token in territory database
$this->wpdb->insert(
$this->wpdb->prefix . 'oauth_access_tokens',
[
'access_token' => $scent_token,
'client_id' => $client_id,
'user_id' => $user_id,
'expires' => $expires,
'scope' => $scope
]
);
return $scent_token;
}
/**
* Generate refresh scent for long-term authentication
*/
private function generate_refresh_scent(string $client_id, int $user_id, string $scope): string {
// 🔐 SECURITY: Higher entropy for refresh tokens (64 bytes = 512 bits)
$refresh_scent = $this->generate_secure_token(64);
$expires = date('Y-m-d H:i:s', time() + (30 * 24 * 3600)); // 30 day scent memory
// Store refresh scent in territory database
$this->wpdb->insert(
$this->wpdb->prefix . 'oauth_refresh_tokens',
[
'refresh_token' => $refresh_scent,
'client_id' => $client_id,
'user_id' => $user_id,
'expires' => $expires,
'scope' => $scope
]
);
return $refresh_scent;
}
/**
* Analyze scent token validity and return data
*/
public function analyze_scent_token(string $token): ?array {
$result = $this->wpdb->get_row(
$this->wpdb->prepare(
"SELECT * FROM {$this->wpdb->prefix}oauth_access_tokens WHERE access_token = %s",
$token
),
ARRAY_A
);
if (!$result) {
return ['active' => false];
}
// Check if scent has expired
if (strtotime($result['expires']) < time()) {
// Clean up expired scent
$this->wpdb->delete(
$this->wpdb->prefix . 'oauth_access_tokens',
['access_token' => $token]
);
return ['active' => false];
}
return [
'active' => true,
'client_id' => $result['client_id'],
'user_id' => (int)$result['user_id'],
'scope' => $result['scope'],
'exp' => strtotime($result['expires'])
];
}
/**
* Show territory authorization form with cat-themed UI
*/
private function show_territory_authorization_form(array $params): void {
// Set content type
header('Content-Type: text/html; charset=utf-8');
// TigerStyle Scent authorization form with cat theming
?>
<!DOCTYPE html>
<html>
<head>
<title>TigerStyle Territory Access</title>
<style>
body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #ff6b35, #f7931e); margin: 0; padding: 20px; }
.scent-form { max-width: 500px; margin: 50px auto; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
.tiger-header { text-align: center; color: #ff6b35; margin-bottom: 20px; }
.scent-info { background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 20px 0; }
.territory-buttons { display: flex; gap: 10px; justify-content: center; margin-top: 20px; }
.scent-button { padding: 12px 24px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
.authorize-btn { background: #28a745; color: white; }
.deny-btn { background: #dc3545; color: white; }
.scent-button:hover { opacity: 0.9; transform: translateY(-1px); }
.paw-icon { font-size: 24px; }
</style>
</head>
<body>
<div class="scent-form">
<div class="tiger-header">
<div class="paw-icon">🐾</div>
<h2>TigerStyle Territory Access</h2>
<p>The application "<strong><?php echo esc_html($params['client']['client_name']); ?></strong>" wants to recognize your scent.</p>
</div>
<div class="scent-info">
<strong>🏰 Requested Territory Permissions:</strong>
<ul>
<?php foreach (explode(' ', $params['scope']) as $scope): ?>
<li><?php echo esc_html($scope); ?></li>
<?php endforeach; ?>
</ul>
</div>
<form method="post">
<?php wp_nonce_field('tigerstyle_scent_authorize'); ?>
<input type="hidden" name="client_id" value="<?php echo esc_attr($params['client_id']); ?>">
<input type="hidden" name="redirect_uri" value="<?php echo esc_attr($params['redirect_uri']); ?>">
<input type="hidden" name="scope" value="<?php echo esc_attr($params['scope']); ?>">
<input type="hidden" name="state" value="<?php echo esc_attr($params['state']); ?>">
<div class="territory-buttons">
<button type="submit" name="authorize" class="scent-button authorize-btn">
🐯 Grant Territory Access
</button>
<button type="submit" name="deny" class="scent-button deny-btn">
❌ Deny Access
</button>
</div>
</form>
</div>
</body>
</html>
<?php
exit;
}
/**
* Grant territory access and redirect with code
*/
private function grant_territory_access(string $client_id, string $redirect_uri, string $scope, string $state): void {
$user_id = get_current_user_id();
// 🔐 SECURITY: Secure authorization code generation (40 bytes = 320 bits)
$territory_code = $this->generate_secure_token(40);
$expires = date('Y-m-d H:i:s', time() + 600); // 10 minute territory code
// Store territory code
$this->wpdb->insert(
$this->wpdb->prefix . 'oauth_authorization_codes',
[
'authorization_code' => $territory_code,
'client_id' => $client_id,
'user_id' => $user_id,
'redirect_uri' => $redirect_uri,
'expires' => $expires,
'scope' => $scope
]
);
// Build redirect URL with territory code
$redirect_params = [
'code' => $territory_code,
'state' => $state
];
$redirect_url = add_query_arg($redirect_params, $redirect_uri);
wp_redirect($redirect_url);
exit;
}
/**
* Send scent token response
*/
private function send_scent_token_response(string $scent_token, string $refresh_scent, string $scope): void {
$response = [
'access_token' => $scent_token,
'token_type' => 'Bearer', // OAuth2 compliant, but we detect ScentBearer too!
'expires_in' => 3600,
'refresh_token' => $refresh_scent,
'scope' => $scope,
// TigerStyle extension - clients can use either Bearer or ScentBearer
'tigerstyle_token_type' => 'ScentBearer',
'scent_strength' => 'strong' // Cat-themed metadata
];
header('Content-Type: application/json');
header('Cache-Control: no-store');
echo json_encode($response);
exit;
}
/**
* Sanitize error messages to prevent information disclosure
*/
private function get_safe_error_message(string $error_type): string {
$safe_messages = [
'invalid_client' => 'Authentication failed',
'invalid_grant' => 'Request denied',
'invalid_request' => 'Bad request',
'invalid_scope' => 'Access denied',
'unauthorized_client' => 'Unauthorized',
'unsupported_grant_type' => 'Request type not supported',
'unsupported_response_type' => 'Response type not supported',
'unknown_territory' => 'Endpoint not found',
'method_not_allowed' => 'Method not allowed',
'rate_limit_exceeded' => 'Too many requests'
];
return $safe_messages[$error_type] ?? 'Request failed';
}
/**
* Send error response with sanitized messages
*/
private function send_error_response(int $status_code, string $error, string $description): void {
// 🔐 SECURITY: Use sanitized error messages in production
$safe_description = $this->get_safe_error_message($error);
// In debug mode, show detailed errors for development
if (defined('TIGERSTYLE_SCENT_DEBUG') && TIGERSTYLE_SCENT_DEBUG) {
$safe_description = $description;
}
http_response_code($status_code);
header('Content-Type: application/json');
$response = [
'error' => $error,
'error_description' => $safe_description
];
echo json_encode($response);
exit;
}
/**
* Extract client scent credentials from Authorization header
*/
private function extract_client_scent_credentials(): ?array {
$auth_header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (strpos($auth_header, 'Basic ') === 0) {
$credentials = base64_decode(substr($auth_header, 6));
$parts = explode(':', $credentials, 2);
if (count($parts) === 2) {
return [
'client_id' => $parts[0],
'client_secret' => $parts[1],
];
}
}
return null;
}
/**
* Get territory code data
*/
private function get_territory_code(string $code): ?array {
return $this->wpdb->get_row(
$this->wpdb->prepare(
"SELECT * FROM {$this->wpdb->prefix}oauth_authorization_codes WHERE authorization_code = %s",
$code
),
ARRAY_A
);
}
/**
* Delete used territory code
*/
private function delete_territory_code(string $code): void {
$this->wpdb->delete(
$this->wpdb->prefix . 'oauth_authorization_codes',
['authorization_code' => $code]
);
}
/**
* 🔐 SECURITY: Enforce HTTPS for all OAuth2 endpoints
*/
private function enforce_https(): void {
// Check if HTTPS is required in settings
$require_https = $this->settings['require_https'] ?? true;
if ($require_https && !is_ssl()) {
// Log security violation
if (defined('TIGERSTYLE_SCENT_DEBUG') && TIGERSTYLE_SCENT_DEBUG) {
error_log('[TigerStyle Scent Security] HTTP request blocked - HTTPS required');
}
// Return secure error
http_response_code(400);
header('Content-Type: application/json');
echo json_encode([
'error' => 'invalid_request',
'error_description' => 'HTTPS required for OAuth2 endpoints'
]);
exit;
}
}
/**
* 🔐 SECURITY: Add comprehensive security headers
*/
private function add_security_headers(): void {
// Prevent clickjacking
header('X-Frame-Options: DENY');
// XSS protection
header('X-XSS-Protection: 1; mode=block');
// MIME type sniffing protection
header('X-Content-Type-Options: nosniff');
// Referrer policy for privacy
header('Referrer-Policy: strict-origin-when-cross-origin');
// Content Security Policy for OAuth2 endpoints
header("Content-Security-Policy: default-src 'none'; script-src 'none'; object-src 'none'; base-uri 'none';");
// HSTS header for HTTPS enforcement (1 year)
if (is_ssl()) {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}
// Cache control for sensitive OAuth2 responses
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
// Feature policy to disable potentially dangerous features
header('Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()');
}
/**
* 🔐 SECURITY: Generate cryptographically secure tokens with maximum entropy
*
* @param int $bytes Number of random bytes (determines entropy)
* @return string Base64URL encoded secure token
*/
private function generate_secure_token(int $bytes): string {
// Generate cryptographically secure random bytes
$random_bytes = random_bytes($bytes);
// Add additional entropy sources for maximum security
$entropy_sources = [
microtime(true),
wp_salt('auth'),
wp_salt('secure_auth'),
$_SERVER['HTTP_USER_AGENT'] ?? '',
$_SERVER['REMOTE_ADDR'] ?? '',
wp_generate_uuid4(),
];
// Combine entropy sources
$additional_entropy = hash('sha256', json_encode($entropy_sources), true);
// Mix random bytes with additional entropy using HMAC
$mixed_entropy = hash_hmac('sha256', $random_bytes, $additional_entropy, true);
// Use base64url encoding for safe URL transmission
return rtrim(strtr(base64_encode($mixed_entropy . $random_bytes), '+/', '-_'), '=');
}
/**
* 🔐 SECURITY: Check if client IP is blocked
*/
private function is_client_blocked(): bool {
$client_ip = $this->get_client_ip();
// Check emergency blocks
$emergency_block = get_transient("tigerstyle_scent_emergency_block_{$client_ip}");
if ($emergency_block) {
return true;
}
// Check rate limit blocks
$rate_limit_block = get_transient("tigerstyle_scent_rate_block_{$client_ip}");
if ($rate_limit_block) {
return true;
}
return false;
}
/**
* 🔐 SECURITY: Send blocked response
*/
private function send_blocked_response(): void {
// Log the blocked attempt
TigerStyleScent_SecurityLogger::log_security_event(
TigerStyleScent_SecurityLogger::EVENT_SECURITY_VIOLATION,
TigerStyleScent_SecurityLogger::SEVERITY_HIGH,
'Blocked IP attempted access',
['blocked_ip' => $this->get_client_ip()]
);
http_response_code(403);
header('Content-Type: application/json');
echo json_encode([
'error' => 'access_denied',
'error_description' => 'Access temporarily restricted'
]);
exit;
}
/**
* 🔐 SECURITY: Validate request integrity
*/
private function validate_request_integrity(): bool {
// Check request size limits
if (!$this->validate_request_size()) {
return false;
}
// Validate content type for POST requests
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$this->validate_content_type()) {
return false;
}
// Check for suspicious request patterns
if ($this->detect_suspicious_patterns()) {
return false;
}
return true;
}
/**
* 🔐 SECURITY: Validate request size limits
*/
private function validate_request_size(): bool {
$max_content_length = 1024 * 1024; // 1MB limit
$content_length = $_SERVER['CONTENT_LENGTH'] ?? 0;
if ($content_length > $max_content_length) {
$this->log_security_violation('Request size exceeded limit', [
'content_length' => $content_length,
'max_allowed' => $max_content_length
]);
return false;
}
return true;
}
/**
* 🔐 SECURITY: Validate content type for POST requests
*/
private function validate_content_type(): bool {
$content_type = $_SERVER['CONTENT_TYPE'] ?? '';
$allowed_types = [
'application/x-www-form-urlencoded',
'application/json',
'multipart/form-data'
];
foreach ($allowed_types as $type) {
if (strpos($content_type, $type) === 0) {
return true;
}
}
$this->log_security_violation('Invalid content type', [
'content_type' => $content_type,
'allowed_types' => $allowed_types
]);
return false;
}
/**
* 🔐 SECURITY: Detect suspicious request patterns
*/
private function detect_suspicious_patterns(): bool {
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
// Patterns that indicate potential attacks
$suspicious_patterns = [
// Directory traversal
'/\.\.[\/\\\\]/',
// SQL injection patterns
'/union\s+select/i',
'/drop\s+table/i',
// Script injection
'/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/i',
// Command injection
'/[;&|`$]/',
// Null bytes
'/\x00/',
];
foreach ($suspicious_patterns as $pattern) {
if (preg_match($pattern, $request_uri) || preg_match($pattern, $user_agent)) {
$this->log_security_violation('Suspicious request pattern detected', [
'pattern' => $pattern,
'request_uri' => $request_uri,
'user_agent' => substr($user_agent, 0, 255)
]);
return true;
}
}
return false;
}
/**
* 🔐 SECURITY: Enhanced security headers
*/
private function add_security_headers(): void {
// Prevent clickjacking
header('X-Frame-Options: DENY');
// Prevent MIME sniffing
header('X-Content-Type-Options: nosniff');
// XSS protection
header('X-XSS-Protection: 1; mode=block');
// Referrer policy
header('Referrer-Policy: strict-origin-when-cross-origin');
// Content Security Policy for OAuth2 endpoints
header("Content-Security-Policy: default-src 'none'; script-src 'none'; style-src 'none'; img-src 'none'");
// Force HTTPS (HSTS)
if (is_ssl()) {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}
// Custom security header for identification
header('X-Security-Exemplar: TigerStyle-Scent-OAuth2');
// Prevent caching of sensitive OAuth2 responses
header('Cache-Control: no-store, no-cache, must-revalidate, private');
header('Pragma: no-cache');
header('Expires: 0');
}
/**
* 🔐 SECURITY: Log security violations
*/
private function log_security_violation(string $message, array $context = []): void {
TigerStyleScent_SecurityLogger::log_security_event(
TigerStyleScent_SecurityLogger::EVENT_SECURITY_VIOLATION,
TigerStyleScent_SecurityLogger::SEVERITY_HIGH,
$message,
$context
);
}
/**
* 🔐 SECURITY: Get client IP with proxy support
*/
private function get_client_ip(): string {
// Check for IP from headers (in order of preference)
$ip_headers = [
'HTTP_CF_CONNECTING_IP', // Cloudflare
'HTTP_X_FORWARDED_FOR', // General proxy
'HTTP_X_REAL_IP', // Nginx proxy
'HTTP_X_FORWARDED', // Squid proxy
'HTTP_FORWARDED_FOR', // Legacy
'HTTP_FORWARDED', // RFC 7239
'REMOTE_ADDR' // Direct connection
];
foreach ($ip_headers as $header) {
$ip = $_SERVER[$header] ?? '';
if (!empty($ip)) {
// Handle comma-separated IPs (take first one)
$ip = trim(explode(',', $ip)[0]);
// Validate IP address
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
// Fallback to REMOTE_ADDR (may be private/local IP)
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
}
// Additional methods for refresh tokens, client credentials, etc. would follow the same cat-themed pattern...
}