PluginProbe
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More / 2.0.2
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More v2.0.2
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.10 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 47 releases
content-control / vendor-prefixed / trustedlogin / client / src / SecurityChecks.php

SecurityChecks.php in Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More 2.0.2, at vendor-prefixed/trustedlogin/client/src/SecurityChecks.php

369 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class SecurityChecks
4 *
5 * @package GravityView\TrustedLogin\Client
6 *
7 * @copyright 2021 Katz Web Services, Inc.
8 *
9 * @license GPL-2.0-or-later
10 * Modified by code-atlantic on 18-September-2023 using Strauss.
11 * @see https://github.com/BrianHenryIE/strauss
12 */
13 namespace ContentControl\Vendor\TrustedLogin;
14
15 use \WP_Error;
16
17 final class SecurityChecks {
18
19 /**
20 * @var Logging $logging
21 */
22 private $logging;
23
24 /**
25 * @var Config $config
26 */
27 private $config;
28
29 /**
30 * @var string The transient slug used for storing used accesskeys.
31 */
32 private $used_accesskey_transient;
33
34 /**
35 * @var string The transient slug used for noting if we're temporarily blocking access.
36 */
37 private $in_lockdown_transient;
38
39 /**
40 * @var int The number of incorrect access keys that should trigger an anomaly alert.
41 */
42 const ACCESSKEY_LIMIT_COUNT = 3;
43
44 /**
45 * @var int The number of seconds we should keep incorrect access keys stored for.
46 */
47 const ACCESSKEY_LIMIT_EXPIRY = 36000; // 10 * MINUTE_IN_SECONDS;
48
49 /**
50 * @var int The number of seconds should block trustedlogin auto-logins for.
51 */
52 const LOCKDOWN_EXPIRY = 72000; // 20 * MINUTE_IN_SECONDS;
53
54 /**
55 * @var string TrustedLogin endpoint to notify brute-force activity
56 */
57 const BRUTE_FORCE_ENDPOINT = 'report-brute-force';
58
59 /**
60 * @var string TrustedLogin endpoint to verify valid support activity
61 */
62 const VERIFY_SUPPORT_AGENT_ENDPOINT = 'verify-identifier';
63
64 public function __construct( Config $config, Logging $logging ) {
65
66 $this->logging = $logging;
67 $this->config = $config;
68
69 $this->used_accesskey_transient = 'tl-' . $this->config->ns() . '-used_accesskeys';
70 $this->in_lockdown_transient = 'tl-' . $this->config->ns() . '-in_lockdown';
71 }
72
73 /**
74 * Verifies that a provided user identifier is still valid.
75 *
76 * Multiple security checks are performed, including brute-force and known-attacker-list checks
77 *
78 * @param string $passed_user_identifier The identifier provided via {@see SupportUser::maybe_login()}
79 *
80 * @return true|WP_Error True if identifier passes checks. WP_Error if not.
81 */
82 public function verify( $passed_user_identifier = '' ) {
83
84 $user_identifier = $passed_user_identifier;
85
86 if ( $this->in_lockdown() ){
87
88 $this->logging->log( 'Site is in lockdown mode, aborting login.', __METHOD__, 'error' );
89
90 return new \WP_Error( 'in_lockdown', __( 'TrustedLogin temporarily disabled.' , 'trustedlogin') );
91 }
92
93 // When passed in the endpoint URL, the unique ID will be the raw value, not the hash.
94 if ( strlen( $passed_user_identifier ) > 32 ) {
95 $user_identifier = Encryption::hash( $passed_user_identifier );
96 }
97
98 $brute_force = $this->check_brute_force( $user_identifier );
99
100 if ( is_wp_error( $brute_force ) ) {
101
102 $this->do_lockdown();
103
104 return $brute_force;
105 }
106
107 $SupportUser = new SupportUser( $this->config, $this->logging );
108
109 $secret_id = $SupportUser->get_secret_id( $user_identifier );
110
111 $approved = $this->check_approved_identifier( $secret_id );
112
113 // Don't lock-down the site, since there could have been errors related to remote validation
114 if ( is_wp_error( $approved ) ){
115
116 $this->logging->log(
117 sprintf(
118 // translators: %s is the error message
119 __( 'There was an issue verifying the user identifier with TrustedLogin, aborting login. (%s)', 'trustedlogin' ),
120 $approved->get_error_message()
121 ),
122 __METHOD__,
123 'error'
124 );
125
126 return $approved;
127 }
128
129 return true;
130 }
131
132 /**
133 * Detects if this identifier indicates that the site's access keys may be under a brute force attack.
134 *
135 * @param string $identifier The identifier provided via {@see Endpoint::maybe_login_support()}
136 *
137 * @return true|WP_Error WP_Error if an anomaly was detected and site may be under attack. Else true.
138 */
139 private function check_brute_force( $identifier ) {
140
141 if ( $this->in_local_development() ) {
142 return true;
143 }
144
145 $used_accesskeys = $this->maybe_add_used_accesskey( $identifier );
146
147 // Is the number of attempted accesses below the lockdown limit?
148 if ( count( $used_accesskeys ) >= self::ACCESSKEY_LIMIT_COUNT ) {
149
150 $this->logging->log(
151 'Potential Brute Force attack detected with identifier: ' . esc_attr( $identifier ),
152 __METHOD__,
153 'notice'
154 );
155
156 return new \WP_Error( 'brute_force_detected', 'Login aborted due to potential brute force detection.');
157 }
158
159 return true;
160 }
161
162 /**
163 * @param string $user_identifier
164 *
165 * @return mixed
166 */
167 private function maybe_add_used_accesskey( $user_identifier = '' ) {
168
169 $used_accesskeys = (array) get_site_transient( $this->used_accesskey_transient );
170
171 // This is a new access key
172 if ( ! in_array( $user_identifier, $used_accesskeys, true ) ) {
173
174 $used_accesskeys[] = $user_identifier;
175
176 $transient_set = set_site_transient( $this->used_accesskey_transient, $used_accesskeys, self::ACCESSKEY_LIMIT_EXPIRY );
177
178 if ( ! $transient_set ) {
179 $this->logging->log( 'Used access key transient not properly set/updated.', __METHOD__, 'error' );
180 }
181
182 }
183
184 return $used_accesskeys;
185 }
186
187 /**
188 * Returns the IP address of the requester
189 *
190 * @return null|string Returns null if REMOTE_ADDR isn't set, string IP address otherwise.
191 */
192 private function get_ip() {
193
194 if ( ! isset( $_SERVER['REMOTE_ADDR'] ) ) {
195 return null;
196 }
197
198 $ip = wp_unslash( $_SERVER['REMOTE_ADDR'] );
199
200 $ip = trim( $ip );
201
202 if ( ! defined('TL_DOING_TESTS') ) {
203 $ip = filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE );
204 }
205
206 return (string) $ip;
207 }
208
209 /**
210 * Makes double-y sure the TrustedLogin Server approves this support-agent login.
211 *
212 * This function sends server variables to the TrustedLogin server to help prevent a number of attack vectors.
213 * It is *only* ever triggered as part of the auto-login sequence.
214 * The session data synced will only ever be from authorized support teams, or potential attackers.
215 *
216 * @param string $secret_id The secret ID for the site.
217 *
218 * @return true|WP_Error True: the TrustedLogin service was reached and the login remains valid. WP_Error: The service wasn't reachable or the service responded that the secret ID wasn't valid.
219 */
220 private function check_approved_identifier( $secret_id ) {
221
222 /**
223 * This array contains information from the Vendor's support agent
224 * as a means of protecting against potential breaches.
225 *
226 * No site user/visitor/admin data is sent back to TrustedLogin server.
227 */
228 $body = array(
229 'timestamp' => time(),
230 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? substr( $_SERVER['HTTP_USER_AGENT'], 0, 255 ) : '',
231 'user_ip' => $this->get_ip(),
232 'site_url' => get_site_url(),
233 );
234
235 $remote = new Remote( $this->config, $this->logging );
236
237 $api_response = $remote->send( 'sites/' . $secret_id . '/' . self::VERIFY_SUPPORT_AGENT_ENDPOINT, $body, 'POST' );
238
239 if ( is_wp_error( $api_response ) ) {
240 return $api_response;
241 }
242
243 $response = $remote->handle_response( $api_response );
244
245 if ( is_wp_error( $response ) ) {
246 return $response;
247 }
248
249 return true;
250 }
251
252 /**
253 * Notifies the TrustedLogin server that a site may be under a possible brute-force attack.
254 *
255 * @since 1.0.0
256 *
257 * @return true|WP_Error If the notification was sent, returns true, otherwise WP_Error on issue.
258 */
259 private function report_lockdown() {
260
261 /**
262 * This array contains identifiable information of either a malicious actor
263 * or the Vendor's support agent who is triggering the alert.
264 *
265 * No site user/visitor/admin data is sent back to TrustedLogin server.
266 */
267 $body = array(
268 'timestamp' => time(),
269 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? substr( $_SERVER['HTTP_USER_AGENT'], 0, 255 ) : '',
270 'user_ip' => $this->get_ip(),
271 'site_url' => get_site_url(),
272 );
273
274 $remote = new Remote( $this->config, $this->logging );
275 $api_response = $remote->send( self::BRUTE_FORCE_ENDPOINT , $body, 'POST' );
276
277 if ( is_wp_error( $api_response ) ) {
278 return $api_response;
279 }
280
281 $response = $remote->handle_response( $api_response );
282
283 if ( is_wp_error( $response ) ) {
284 return $response;
285 }
286
287 return true;
288
289 }
290
291 /**
292 * Locks down the site to new access by TrustedLogin identifiers, reports lockdown to TrustedLogin
293 */
294 private function do_lockdown() {
295
296 $this->logging->log( 'Brute force is detected; starting lockdown.', __METHOD__, 'emergency' );
297
298 $transient_set = set_site_transient( $this->in_lockdown_transient, time(), self::LOCKDOWN_EXPIRY );
299
300 if ( ! $transient_set ) {
301 $this->logging->log( 'Could not set the "in lockdown" transient.', __METHOD__, 'alert' );
302 }
303
304 $notified = $this->report_lockdown();
305
306 if ( is_wp_error( $notified ) ){
307 $this->logging->log( sprintf( 'Could not notify TrustedLogin (%s)', $notified->get_error_message() ), __METHOD__, 'error' );
308 }
309
310 /**
311 * Runs after the site is locked down to access from the Vendor
312 */
313 do_action( 'trustedlogin/' . $this->config->ns() . '/lockdown/after' );
314 }
315
316 /**
317 * Is this site in local development mode?
318 *
319 * @uses \wp_get_environment_type() If available, used to fetch site's development environment
320 *
321 * @see https://developer.wordpress.org/reference/functions/wp_get_environment_type/
322 *
323 * To bypass lockdown checks, set a WordPress environment to `local` or `development`. Alternately, you may
324 * add a constant to the site's wp-config.php file formatted as `TRUSTEDLOGIN_TESTING_{EXAMPLE}` where
325 * `{EXAMPLE}` is replaced with the project's upper-cased namespace.
326 *
327 * @return bool True: site is in local or development environment. False: site is live.
328 */
329 private function in_local_development() {
330
331 $constant_name = 'TRUSTEDLOGIN_TESTING_' . strtoupper( $this->config->ns() );
332
333 if ( defined( $constant_name ) && constant( $constant_name ) ) {
334 return true;
335 }
336
337 if ( ! function_exists( 'wp_get_environment_type' ) ) {
338 return false;
339 }
340
341 switch ( wp_get_environment_type() ) {
342 case 'local':
343 case 'development':
344 return true;
345 break;
346 case 'staging':
347 case 'production':
348 default:
349 }
350
351 return false;
352 }
353
354 /**
355 * Checks if TrustedLogin is currently in lockdown
356 *
357 * @return int|false Int: in lockdown. The value returned is the timestamp when lockdown ends. False: not in lockdown, or overridden by a constant.
358 */
359 public function in_lockdown(){
360
361 if ( $this->in_local_development() ) {
362 return false;
363 }
364
365 return get_site_transient( $this->in_lockdown_transient );
366 }
367
368 }
369