PluginProbe
Easy Basic Authentication – Add basic auth to site or admin area / 4.2.0
Easy Basic Authentication – Add basic auth to site or admin area v4.2.0
4.2.0 4.1.0 trunk 1.1 1.2 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 1.6.1 1.6.2 1.6.3 1.6.4 1.7 1.7.1 1.8 All 52 releases
easy-basic-authentication / class / easy-basic-authentication-class.php

easy-basic-authentication-class.php in Easy Basic Authentication – Add basic auth to site or admin area 4.2.0, at class/easy-basic-authentication-class.php

423 lines 15.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4 }
5
6
7 require_once (dirname(__FILE__).'/easy-basic-authentication-log-class.php');
8 require_once (dirname(__FILE__).'/easy-basic-authentication-emailalert-class.php');
9 require_once (dirname(__FILE__).'/easy-basic-authentication-form-class.php');
10 require_once (dirname(__FILE__).'/easy-basic-authentication-notice-class.php');
11 require_once (dirname(__FILE__).'/easy-basic-authentication-compatcheck-class.php');
12
13 class easy_basic_authentication_class {
14
15 private $log;
16 private $email;
17 private $form;
18 private $compatcheck;
19
20 public function __construct()
21 {
22 $this->log = new easy_basic_authentication_log_class();
23 $this->email = new easy_basic_authentication_emailalert_class();
24 $this->form = new easy_basic_authentication_form_class();
25 $notice = new easy_basic_authentication_notice_class();
26 $this->compatcheck = new easy_basic_authentication_compatcheck_class();
27 $this->compatcheck->register_hooks();
28
29 // Fuori da una richiesta HTTP la sfida 401 non ha alcun senso: da riga di comando
30 // diventa un exit() silenzioso che blocca WP-CLI, il cron di sistema e qualunque
31 // script che faccia require di wp-load.php.
32 if ( ! self::is_cli() ) {
33 $pagenow = isset( $GLOBALS['pagenow'] ) ? $GLOBALS['pagenow'] : '';
34
35 if(get_option( 'basic_auth_plugin_admin_enable' )) {
36 if (in_array($pagenow, array('wp-login.php', 'wp-register.php'))) {
37 add_action( 'init', array($this,'basic_auth_root') );
38 }
39 }
40
41 if(get_option( 'basic_auth_plugin_enable' ) && get_option( 'basic_auth_plugin_admin_enable' )){
42 add_action( 'init', array($this,'basic_auth_root') );
43 }
44 }
45
46 add_action( 'init', array($this, 'maybe_upgrade'), 1 );
47
48 add_action( 'admin_menu', array($this,'basic_auth_plugin_menu' ));
49 add_action( 'admin_init', array($this->form,'basic_auth_plugin_settings_init' ));
50
51 add_action('admin_init', function () {
52 if ( ! current_user_can( 'manage_options' ) ) {
53 return;
54 }
55
56 // Il nonce e verificato dentro basic_auth_plugin_save_settings(), che riceve
57 // i dati grezzi perche deve distinguere un campo assente da uno vuoto (le
58 // checkbox non spuntate non vengono inviate).
59 // phpcs:ignore WordPress.Security.NonceVerification.Missing
60 $post_data = $_POST;
61 $this->form->basic_auth_plugin_save_settings($post_data);
62 });
63
64 }
65
66 /**
67 * Se stiamo girando fuori da una richiesta HTTP (WP-CLI, cron di sistema, script).
68 *
69 * @return bool
70 */
71 public static function is_cli() {
72 if ( defined( 'WP_CLI' ) && WP_CLI ) {
73 return true;
74 }
75
76 return 'cli' === PHP_SAPI || 'phpdbg' === PHP_SAPI;
77 }
78
79 public function basic_auth_root()
80 {
81 if ( self::is_cli() ) {
82 return;
83 }
84
85 // wp-cron.php chiude la risposta con fastcgi_finish_request() prima ancora di
86 // caricare WordPress: quando arriviamo qui gli header sono gia partiti. La sfida
87 // 401 non raggiunge nessuno, lascia un warning "headers already sent" nel log a
88 // ogni giro, e l'exit che la segue ferma tutte le azioni pianificate del sito.
89 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
90 return;
91 }
92
93 $user = get_option('basic_auth_plugin_username');
94 $pass = get_option('basic_auth_plugin_password');
95
96 if ($this->whiteListChecker()) {
97 return;
98 }
99
100 if ($this->urlWhiteListChecker()) {
101 return;
102 }
103
104 if ($this->tokenChecker()) {
105 return;
106 }
107
108 // Alcuni server (nginx + FastCGI su tutti) non popolano PHP_AUTH_USER da soli:
109 // le credenziali arrivano solo nell'header Authorization e vanno estratte a mano.
110 if (!isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['HTTP_AUTHORIZATION'])) {
111 $authorization = sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
112
113 if ( preg_match( '#^Basic\s+([A-Za-z0-9+/=]+)$#i', $authorization, $matches ) ) {
114 $decoded = base64_decode( $matches[1], true );
115
116 // Senza i due punti non ci sono due campi: la list() originale
117 // generava un warning "Undefined array key 1" su PHP 8.
118 if ( false !== $decoded && false !== strpos( $decoded, ':' ) ) {
119 list( $sent_user, $sent_pass ) = explode( ':', $decoded, 2 );
120 $_SERVER['PHP_AUTH_USER'] = $sent_user;
121 $_SERVER['PHP_AUTH_PW'] = $sent_pass;
122 }
123 }
124 }
125
126 // Basic Auth prevede sempre un primo giro senza credenziali: il browser chiede,
127 // riceve 401, e solo allora rimanda con utente e password. Quel 401 non e un
128 // tentativo fallito, e registrarlo significherebbe una voce di log (e una mail)
129 // per ogni visitatore e ogni bot che passa.
130 $credentials_sent = isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']);
131
132 if (!$credentials_sent) {
133 $this->do_exit(true, false);
134 return;
135 }
136
137 // L'utente e salvato passando per sanitize_text_field(), quindi quello in arrivo
138 // va normalizzato allo stesso modo perche il confronto sia sensato.
139 $sent_user = sanitize_text_field( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) );
140
141 // La password invece NON va sanitizzata: alterarla farebbe fallire l'accesso a
142 // chiunque ne usi una con caratteri speciali. Non viene mai stampata, solo
143 // confrontata con l'hash.
144 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
145 $sent_pass = wp_unslash( $_SERVER['PHP_AUTH_PW'] );
146
147 $credentials_valid = hash_equals( (string) $user, $sent_user )
148 && wp_check_password( $sent_pass, $pass );
149
150 if (!$credentials_valid) {
151 // Credenziali inviate e sbagliate: questo si che va segnalato.
152 $this->do_exit(true, true);
153 }
154 }
155
156 public function urlWhiteListChecker() {
157 if (empty($_SERVER['HTTP_HOST']) || empty($_SERVER['REQUEST_URI'])) {
158 return false;
159 }
160
161 $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
162
163 $host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );
164
165 // L'URI serve solo al confronto con la whitelist, non viene mai stampato.
166 // sanitize_text_field() qui sarebbe dannoso: rimuove le sequenze %xx e
167 // cambierebbe il confronto su qualsiasi percorso con caratteri codificati.
168 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
169 $request_uri = wp_unslash( $_SERVER['REQUEST_URI'] );
170
171 $currentUrl = $scheme . '://' . $host . $request_uri;
172
173 $whitelist = $this->getUrlWhiteList();
174
175 foreach ($whitelist as $entry) {
176 if ($this->isUrlAllowed($currentUrl, $entry)) {
177 return true;
178 }
179 }
180
181 return false;
182 }
183
184 private function isUrlAllowed($currentUrl, $entry) {
185 $currentUrl = rtrim($currentUrl, '/');
186 $entry = rtrim($entry, '/');
187
188 if (strpos($entry, '/') === 0) {
189 $path = wp_parse_url($currentUrl, PHP_URL_PATH);
190 return stripos($path, $entry) === 0;
191 }
192
193 if (!preg_match('#^https?://#i', $entry)) {
194 $scheme = wp_parse_url($currentUrl, PHP_URL_SCHEME) ?: 'https';
195 $entry = $scheme . '://' . $entry;
196 }
197
198 return stripos($currentUrl, $entry) === 0;
199 }
200
201 public function whiteListChecker() {
202 if (!isset($_SERVER['REMOTE_ADDR'])) {
203 return false;
204 }
205
206 // Un REMOTE_ADDR malformato non deve arrivare ai confronti della whitelist:
207 // ip2long() restituirebbe false e il risultato sarebbe imprevedibile.
208 $ip = filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP );
209
210 if ( false === $ip ) {
211 return false;
212 }
213
214 $whitelist = $this->getWhiteList();
215
216 foreach ($whitelist as $entry) {
217 if ($this->isIpAllowed($ip, $entry)) {
218 return true;
219 }
220 }
221 return false;
222 }
223
224 private function isIpAllowed($ip, $entry) {
225 if (filter_var($entry, FILTER_VALIDATE_IP)) {
226 return $ip === $entry;
227 } elseif (strpos($entry, '/') !== false) {
228 return $this->isIpInCidr($ip, $entry);
229 } elseif (strpos($entry, '-') !== false) {
230 return $this->isIpInRange($ip, $entry);
231 }
232 return false;
233 }
234
235 private function isIpInCidr($ip, $cidr) {
236 list($subnet, $mask) = explode('/', $cidr);
237 $ipLong = ip2long($ip);
238 $subnetLong = ip2long($subnet);
239 $maskLong = -1 << (32 - $mask);
240 return ($ipLong & $maskLong) === ($subnetLong & $maskLong);
241 }
242
243 private function isIpInRange($ip, $range) {
244 list($start, $end) = array_map('trim', explode('-', $range));
245 $ipLong = ip2long($ip);
246 $startLong = ip2long($start);
247 $endLong = ip2long($end);
248 return ($ipLong >= $startLong && $ipLong <= $endLong);
249 }
250
251 /**
252 * Un crawler o un servizio esterno sta passando col token d'accesso?
253 *
254 * Serve a far entrare chi non puo usare l'autenticazione HTTP: un crawler, un
255 * controllo di raggiungibilita, un front-end headless. Il token vale solo per
256 * il sito: l'amministrazione e la pagina di accesso restano protette, perche
257 * un crawler non ha motivo di entrarci e aprirgliele sarebbe rischio gratuito.
258 *
259 * @return bool
260 */
261 public function tokenChecker() {
262 $token = (string) get_option('basic_auth_plugin_access_token', '');
263
264 // Funzione spenta finche non si genera un token.
265 if ('' === $token) {
266 return false;
267 }
268
269 // Mai per l'amministrazione o la pagina di accesso.
270 if (is_admin() || $this->isLoginRequest()) {
271 return false;
272 }
273
274 $sent = $this->getRequestToken();
275
276 if ('' === $sent) {
277 return false;
278 }
279
280 // hash_equals: il confronto non deve rivelare quanti caratteri erano giusti.
281 if (!hash_equals($token, $sent)) {
282 // Un token sbagliato e un tentativo deliberato, non il primo giro a vuoto
283 // del protocollo: va segnalato come una password sbagliata.
284 $this->basic_auth_action_failed_access();
285
286 return false;
287 }
288
289 /**
290 * Scatta quando una richiesta entra col token invece che con le credenziali.
291 */
292 do_action('basic_auth_token_access_granted');
293
294 return true;
295 }
296
297 /**
298 * Il token inviato con la richiesta, da uno dei due header accettati.
299 *
300 * @return string
301 */
302 private function getRequestToken() {
303 // Header dedicato: la strada piu pulita.
304 if (!empty($_SERVER['HTTP_X_BASIC_AUTH_TOKEN'])) {
305 return trim(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_BASIC_AUTH_TOKEN'])));
306 }
307
308 // Authorization: Bearer, per i servizi che sanno mandare solo questo.
309 if (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
310 $header = sanitize_text_field(wp_unslash($_SERVER['HTTP_AUTHORIZATION']));
311
312 if (preg_match('#^Bearer\s+(\S+)$#i', $header, $matches)) {
313 return $matches[1];
314 }
315 }
316
317 return '';
318 }
319
320 /**
321 * Se la richiesta corrente e la pagina di accesso.
322 *
323 * @return bool
324 */
325 private function isLoginRequest() {
326 $pagenow = isset($GLOBALS['pagenow']) ? $GLOBALS['pagenow'] : '';
327
328 if (in_array($pagenow, array('wp-login.php', 'wp-register.php'), true)) {
329 return true;
330 }
331
332 if (empty($_SERVER['REQUEST_URI'])) {
333 return false;
334 }
335
336 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- solo confronto, mai stampato.
337 $uri = wp_unslash($_SERVER['REQUEST_URI']);
338
339 return false !== stripos($uri, 'wp-login.php') || false !== stripos($uri, 'wp-register.php');
340 }
341
342 /**
343 * Manda la sfida 401.
344 *
345 * @param bool $admin_area Se la richiesta riguarda l'area di amministrazione.
346 * @param bool $log_attempt Se registrare l'accesso fra i tentativi falliti.
347 * False per il 401 iniziale, che fa parte del protocollo.
348 */
349 public function do_exit($admin_area = false, $log_attempt = true) {
350
351 if ($log_attempt) {
352 $this->basic_auth_action_failed_access();
353 }
354
355 do_action('basic_auth_before_401');
356
357 if ($admin_area) {
358 do_action('basic_auth_before_401_admin_area');
359 }
360
361 // I browser memorizzano le credenziali per realm: cambiare questa stringa fa
362 // ricomparire la richiesta di accesso a chi era gia autenticato. Il valore
363 // predefinito resta quello storico; chi vuole cambiarlo usa il filtro.
364 $realm = apply_filters('basic_auth_realm', 'My Website', $admin_area);
365 $realm = str_replace(array('"', "\r", "\n"), '', (string) $realm);
366
367 header('WWW-Authenticate: Basic realm="' . $realm . '"');
368 status_header(401);
369 exit;
370 }
371
372 public function getWhiteList() {
373 return get_option( 'basic_auth_plugin_whitelist' )?explode(',',get_option( 'basic_auth_plugin_whitelist' )):[];
374 }
375
376 public function getUrlWhiteList() {
377 return get_option( 'basic_auth_plugin_urlwhitelist' )?explode(',',get_option( 'basic_auth_plugin_urlwhitelist' )):[];
378 }
379
380 public function basic_auth_plugin_menu() {
381 add_menu_page(
382 __('Configurations for Easy Basic Authentication', 'easy-basic-authentication'),
383 __('Easy Basic A.', 'easy-basic-authentication'),
384 'manage_options',
385 'basic-auth-plugin',
386 array($this->form, 'basic_auth_plugin_settings_page'),
387 'dashicons-lock'
388 );
389 if($this->log->is_enabled()) {
390 $this->log->getMenu();
391 }
392 }
393
394 /**
395 * Migrazioni una tantum, eseguite quando cambia la versione del plugin.
396 *
397 * Fino alla 4.0.0 il log degli accessi stava in un'opzione in autoload: fino a
398 * 500 voci, circa 144 KB, deserializzate a ogni richiesta del sito.
399 */
400 public function maybe_upgrade() {
401 if ( get_option( 'basic_auth_plugin_db_version' ) === EASY_BASIC_AUTHENTICATION_VERSION ) {
402 return;
403 }
404
405 $logs = get_option( 'basic_auth_failure_logs', null );
406 if ( null !== $logs ) {
407 update_option( 'basic_auth_failure_logs', $logs, false );
408 }
409
410 update_option( 'basic_auth_plugin_db_version', EASY_BASIC_AUTHENTICATION_VERSION, false );
411 }
412
413 public function basic_auth_action_failed_access() {
414 if($this->log->is_enabled()) {
415 $this->log->update_status($_SERVER);
416 }
417 if($this->email->is_enabled()) {
418 $this->email->sendAlert($_SERVER);
419 }
420 }
421
422 }
423