PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.8.4
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.8.4
3.0.3 3.0.2 3.0.1 trunk 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.3.0 2.3.1 2.3.10 2.3.11 2.3.12 2.3.13 2.3.14 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.91 2.7.92 2.7.93 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0
commercebird / admin / includes / Security / SecurityMiddleware.php
commercebird / admin / includes / Security Last commit date
SecurityConfig.php 9 months ago SecurityMiddleware.php 6 months ago index.php 10 months ago
SecurityMiddleware.php
229 lines
1 <?php
2 /**
3 * Security Middleware Trait for CommerceBird
4 *
5 * @package CommerceBird\Admin\Security
6 */
7
8 namespace CommerceBird\Admin\Security;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 trait SecurityMiddleware {
15
16 /**
17 * Apply security headers to responses.
18 */
19 private function apply_security_headers(): void {
20 $headers = SecurityConfig::get_security_headers();
21
22 foreach ( $headers as $header => $value ) {
23 if ( ! headers_sent() ) {
24 header( $header . ': ' . $value );
25 }
26 }
27 }
28
29 /**
30 * Validate input data against known patterns.
31 *
32 * @param array $data Input data to validate.
33 * @param array $field_types Field type mappings.
34 * @return bool True if valid, false otherwise.
35 */
36 private function validate_input_patterns( array $data, array $field_types ): bool {
37 foreach ( $field_types as $field => $type ) {
38 if ( ! isset( $data[ $field ] ) ) {
39 continue;
40 }
41
42 $pattern = SecurityConfig::get_validation_pattern( $type );
43 if ( $pattern && ! preg_match( $pattern, $data[ $field ] ) ) {
44 return false;
45 }
46 }
47
48 return true;
49 }
50
51 /**
52 * Check user capability for specific action.
53 *
54 * @param string $action Action being performed.
55 * @return bool True if user has capability, false otherwise.
56 */
57 private function check_user_capability( string $action ): bool {
58 $required_cap = SecurityConfig::get_required_capability( $action );
59
60 if ( ! $required_cap ) {
61 return true; // No specific capability required.
62 }
63
64 return current_user_can( $required_cap );
65 }
66
67 /**
68 * Advanced rate limiting with multiple time windows.
69 *
70 * @param string $action Action being rate limited.
71 * @return bool True if within limits, false if exceeded.
72 */
73 private function check_advanced_rate_limit( string $action ): bool {
74 $user_id = get_current_user_id();
75 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- IP used for rate limiting key.
76 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : 'unknown';
77 $base_key = "rate_limit_{$action}_{$user_id}_{$ip}";
78
79 // Check per-minute limit.
80 $minute_key = $base_key . '_minute';
81 $minute_requests = get_transient( $minute_key );
82
83 if ( false === $minute_requests ) {
84 set_transient( $minute_key, 1, MINUTE_IN_SECONDS );
85 } else {
86 if ( $minute_requests >= SecurityConfig::RATE_LIMIT_REQUESTS_PER_MINUTE ) {
87 return false;
88 }
89 set_transient( $minute_key, $minute_requests + 1, MINUTE_IN_SECONDS );
90 }
91
92 // Check per-hour limit.
93 $hour_key = $base_key . '_hour';
94 $hour_requests = get_transient( $hour_key );
95
96 if ( false === $hour_requests ) {
97 set_transient( $hour_key, 1, HOUR_IN_SECONDS );
98 } else {
99 if ( $hour_requests >= SecurityConfig::RATE_LIMIT_REQUESTS_PER_HOUR ) {
100 return false;
101 }
102 set_transient( $hour_key, $hour_requests + 1, HOUR_IN_SECONDS );
103 }
104
105 return true;
106 }
107
108 /**
109 * Log security event with context.
110 *
111 * @param string $event_type Type of security event.
112 * @param array $context Additional context data.
113 */
114 private function log_security_event( string $event_type, array $context = array() ): void {
115 if ( ! SecurityConfig::LOG_SECURITY_EVENTS ) {
116 return;
117 }
118
119 $event_data = array(
120 'timestamp' => current_time( 'mysql' ),
121 'event_type' => $event_type,
122 'user_id' => get_current_user_id(),
123 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Security logging.
124 'ip_address' => isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : 'unknown',
125 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Security logging.
126 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown',
127 'context' => $context,
128 );
129
130 $security_log = get_transient( 'commercebird_security_log' );
131 if ( ! is_array( $security_log ) ) {
132 $security_log = array();
133 }
134
135 $security_log[] = $event_data;
136
137 // Maintain log size limits.
138 if ( count( $security_log ) > SecurityConfig::LOG_MAX_ENTRIES ) {
139 $security_log = array_slice( $security_log, -SecurityConfig::LOG_MAX_ENTRIES );
140 }
141
142 set_transient( 'commercebird_security_log', $security_log, DAY_IN_SECONDS * SecurityConfig::LOG_RETENTION_DAYS );
143 }
144
145 /**
146 * Sanitize and validate subscription data.
147 *
148 * @param array $data Raw subscription data.
149 * @return array Sanitized and validated data.
150 */
151 private function sanitize_subscription_response( array $data ): array {
152 $allowed_fields = array(
153 'status',
154 'plan',
155 'currency',
156 'total',
157 'needs_payment',
158 'next_payment_date_gmt',
159 'variation_id',
160 );
161
162 $sanitized = array();
163
164 foreach ( $allowed_fields as $field ) {
165 if ( isset( $data[ $field ] ) ) {
166 if ( is_array( $data[ $field ] ) ) {
167 $sanitized[ $field ] = array_map( 'sanitize_text_field', $data[ $field ] );
168 } else {
169 $sanitized[ $field ] = sanitize_text_field( $data[ $field ] );
170 }
171 }
172 }
173
174 // Handle billing data separately with stricter controls.
175 if ( isset( $data['billing'] ) && is_array( $data['billing'] ) ) {
176 $sanitized['billing'] = array();
177
178 // Only include essential billing fields.
179 if ( isset( $data['billing']['email'] ) ) {
180 $email = sanitize_email( $data['billing']['email'] );
181 if ( is_email( $email ) ) {
182 $sanitized['billing']['email'] = $email;
183 }
184 }
185 }
186
187 return $sanitized;
188 }
189
190 /**
191 * Check if request is from a suspicious source.
192 *
193 * @return bool True if suspicious, false otherwise.
194 */
195 private function is_suspicious_request(): bool {
196 // Check for missing or suspicious user agent.
197 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Used for pattern matching.
198 $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
199 if ( empty( $user_agent ) || strlen( $user_agent ) < 10 ) {
200 return true;
201 }
202
203 // Check for suspicious patterns in user agent.
204 $suspicious_patterns = array(
205 '/bot/i',
206 '/crawler/i',
207 '/spider/i',
208 '/scraper/i',
209 );
210
211 foreach ( $suspicious_patterns as $pattern ) {
212 if ( preg_match( $pattern, $user_agent ) ) {
213 return true;
214 }
215 }
216
217 // Check referer header.
218 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Used for URL comparison.
219 $referer = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : '';
220 $site_url = site_url();
221
222 if ( ! empty( $referer ) && strpos( $referer, $site_url ) !== 0 ) {
223 return true;
224 }
225
226 return false;
227 }
228 }
229