PluginProbe
Easy Basic Authentication – Add basic auth to site or admin area / 4.1.0
Easy Basic Authentication – Add basic auth to site or admin area v4.1.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 1.8.1 All 51 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.1.0, at class/easy-basic-authentication-class.php

320 lines 11.6 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 $user = get_option('basic_auth_plugin_username');
86 $pass = get_option('basic_auth_plugin_password');
87
88 if ($this->whiteListChecker()) {
89 return;
90 }
91
92 if ($this->urlWhiteListChecker()) {
93 return;
94 }
95
96 // Alcuni server (nginx + FastCGI su tutti) non popolano PHP_AUTH_USER da soli:
97 // le credenziali arrivano solo nell'header Authorization e vanno estratte a mano.
98 if (!isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['HTTP_AUTHORIZATION'])) {
99 $authorization = sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
100
101 if ( preg_match( '#^Basic\s+([A-Za-z0-9+/=]+)$#i', $authorization, $matches ) ) {
102 $decoded = base64_decode( $matches[1], true );
103
104 // Senza i due punti non ci sono due campi: la list() originale
105 // generava un warning "Undefined array key 1" su PHP 8.
106 if ( false !== $decoded && false !== strpos( $decoded, ':' ) ) {
107 list( $sent_user, $sent_pass ) = explode( ':', $decoded, 2 );
108 $_SERVER['PHP_AUTH_USER'] = $sent_user;
109 $_SERVER['PHP_AUTH_PW'] = $sent_pass;
110 }
111 }
112 }
113
114 // Basic Auth prevede sempre un primo giro senza credenziali: il browser chiede,
115 // riceve 401, e solo allora rimanda con utente e password. Quel 401 non e un
116 // tentativo fallito, e registrarlo significherebbe una voce di log (e una mail)
117 // per ogni visitatore e ogni bot che passa.
118 $credentials_sent = isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']);
119
120 if (!$credentials_sent) {
121 $this->do_exit(true, false);
122 return;
123 }
124
125 // L'utente e salvato passando per sanitize_text_field(), quindi quello in arrivo
126 // va normalizzato allo stesso modo perche il confronto sia sensato.
127 $sent_user = sanitize_text_field( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) );
128
129 // La password invece NON va sanitizzata: alterarla farebbe fallire l'accesso a
130 // chiunque ne usi una con caratteri speciali. Non viene mai stampata, solo
131 // confrontata con l'hash.
132 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
133 $sent_pass = wp_unslash( $_SERVER['PHP_AUTH_PW'] );
134
135 $credentials_valid = hash_equals( (string) $user, $sent_user )
136 && wp_check_password( $sent_pass, $pass );
137
138 if (!$credentials_valid) {
139 // Credenziali inviate e sbagliate: questo si che va segnalato.
140 $this->do_exit(true, true);
141 }
142 }
143
144 public function urlWhiteListChecker() {
145 if (empty($_SERVER['HTTP_HOST']) || empty($_SERVER['REQUEST_URI'])) {
146 return false;
147 }
148
149 $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
150
151 $host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );
152
153 // L'URI serve solo al confronto con la whitelist, non viene mai stampato.
154 // sanitize_text_field() qui sarebbe dannoso: rimuove le sequenze %xx e
155 // cambierebbe il confronto su qualsiasi percorso con caratteri codificati.
156 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
157 $request_uri = wp_unslash( $_SERVER['REQUEST_URI'] );
158
159 $currentUrl = $scheme . '://' . $host . $request_uri;
160
161 $whitelist = $this->getUrlWhiteList();
162
163 foreach ($whitelist as $entry) {
164 if ($this->isUrlAllowed($currentUrl, $entry)) {
165 return true;
166 }
167 }
168
169 return false;
170 }
171
172 private function isUrlAllowed($currentUrl, $entry) {
173 $currentUrl = rtrim($currentUrl, '/');
174 $entry = rtrim($entry, '/');
175
176 if (strpos($entry, '/') === 0) {
177 $path = wp_parse_url($currentUrl, PHP_URL_PATH);
178 return stripos($path, $entry) === 0;
179 }
180
181 if (!preg_match('#^https?://#i', $entry)) {
182 $scheme = wp_parse_url($currentUrl, PHP_URL_SCHEME) ?: 'https';
183 $entry = $scheme . '://' . $entry;
184 }
185
186 return stripos($currentUrl, $entry) === 0;
187 }
188
189 public function whiteListChecker() {
190 if (!isset($_SERVER['REMOTE_ADDR'])) {
191 return false;
192 }
193
194 // Un REMOTE_ADDR malformato non deve arrivare ai confronti della whitelist:
195 // ip2long() restituirebbe false e il risultato sarebbe imprevedibile.
196 $ip = filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP );
197
198 if ( false === $ip ) {
199 return false;
200 }
201
202 $whitelist = $this->getWhiteList();
203
204 foreach ($whitelist as $entry) {
205 if ($this->isIpAllowed($ip, $entry)) {
206 return true;
207 }
208 }
209 return false;
210 }
211
212 private function isIpAllowed($ip, $entry) {
213 if (filter_var($entry, FILTER_VALIDATE_IP)) {
214 return $ip === $entry;
215 } elseif (strpos($entry, '/') !== false) {
216 return $this->isIpInCidr($ip, $entry);
217 } elseif (strpos($entry, '-') !== false) {
218 return $this->isIpInRange($ip, $entry);
219 }
220 return false;
221 }
222
223 private function isIpInCidr($ip, $cidr) {
224 list($subnet, $mask) = explode('/', $cidr);
225 $ipLong = ip2long($ip);
226 $subnetLong = ip2long($subnet);
227 $maskLong = -1 << (32 - $mask);
228 return ($ipLong & $maskLong) === ($subnetLong & $maskLong);
229 }
230
231 private function isIpInRange($ip, $range) {
232 list($start, $end) = array_map('trim', explode('-', $range));
233 $ipLong = ip2long($ip);
234 $startLong = ip2long($start);
235 $endLong = ip2long($end);
236 return ($ipLong >= $startLong && $ipLong <= $endLong);
237 }
238
239 /**
240 * Manda la sfida 401.
241 *
242 * @param bool $admin_area Se la richiesta riguarda l'area di amministrazione.
243 * @param bool $log_attempt Se registrare l'accesso fra i tentativi falliti.
244 * False per il 401 iniziale, che fa parte del protocollo.
245 */
246 public function do_exit($admin_area = false, $log_attempt = true) {
247
248 if ($log_attempt) {
249 $this->basic_auth_action_failed_access();
250 }
251
252 do_action('basic_auth_before_401');
253
254 if ($admin_area) {
255 do_action('basic_auth_before_401_admin_area');
256 }
257
258 // I browser memorizzano le credenziali per realm: cambiare questa stringa fa
259 // ricomparire la richiesta di accesso a chi era gia autenticato. Il valore
260 // predefinito resta quello storico; chi vuole cambiarlo usa il filtro.
261 $realm = apply_filters('basic_auth_realm', 'My Website', $admin_area);
262 $realm = str_replace(array('"', "\r", "\n"), '', (string) $realm);
263
264 header('WWW-Authenticate: Basic realm="' . $realm . '"');
265 status_header(401);
266 exit;
267 }
268
269 public function getWhiteList() {
270 return get_option( 'basic_auth_plugin_whitelist' )?explode(',',get_option( 'basic_auth_plugin_whitelist' )):[];
271 }
272
273 public function getUrlWhiteList() {
274 return get_option( 'basic_auth_plugin_urlwhitelist' )?explode(',',get_option( 'basic_auth_plugin_urlwhitelist' )):[];
275 }
276
277 public function basic_auth_plugin_menu() {
278 add_menu_page(
279 __('Configurations for Easy Basic Authentication', 'easy-basic-authentication'),
280 __('Easy Basic A.', 'easy-basic-authentication'),
281 'manage_options',
282 'basic-auth-plugin',
283 array($this->form, 'basic_auth_plugin_settings_page'),
284 'dashicons-lock'
285 );
286 if($this->log->is_enabled()) {
287 $this->log->getMenu();
288 }
289 }
290
291 /**
292 * Migrazioni una tantum, eseguite quando cambia la versione del plugin.
293 *
294 * Fino alla 4.0.0 il log degli accessi stava in un'opzione in autoload: fino a
295 * 500 voci, circa 144 KB, deserializzate a ogni richiesta del sito.
296 */
297 public function maybe_upgrade() {
298 if ( get_option( 'basic_auth_plugin_db_version' ) === EASY_BASIC_AUTHENTICATION_VERSION ) {
299 return;
300 }
301
302 $logs = get_option( 'basic_auth_failure_logs', null );
303 if ( null !== $logs ) {
304 update_option( 'basic_auth_failure_logs', $logs, false );
305 }
306
307 update_option( 'basic_auth_plugin_db_version', EASY_BASIC_AUTHENTICATION_VERSION, false );
308 }
309
310 public function basic_auth_action_failed_access() {
311 if($this->log->is_enabled()) {
312 $this->log->update_status($_SERVER);
313 }
314 if($this->email->is_enabled()) {
315 $this->email->sendAlert($_SERVER);
316 }
317 }
318
319 }
320