*/ function stoutwall_default_switches() { return [ 'disable_registration' => 1, 'disable_file_editors' => 1, 'prevent_uploads_code_execution' => 1, 'hide_wp_version' => 1, 'prevent_login_feedback' => 1, 'disable_directory_browsing' => 1, 'disable_user_enumeration' => 1, 'unset_x_powered_by' => 1, 'block_admin_username' => 1, 'disable_xmlrpc' => 1, 'block_registration_same_login_display' => 1, 'limit_login_attempts' => 1, 'security_headers' => 1, 'hsts' => 0, 'disable_application_passwords' => 0, 'restrict_rest_api' => 0, ]; } /** * Fontes aceitas para o IP do visitante (usado no limite de tentativas de login). * * @return string[] */ function stoutwall_ip_sources() { return ['remote_addr', 'cf_connecting_ip', 'x_real_ip', 'x_forwarded_for']; } /** * Retorna as opções padrão do plugin. * * @return array */ function stoutwall_default_options() { return stoutwall_default_switches() + [ 'login_ip_source' => 'remote_addr', ]; } /** * Retorna as opções do plugin mescladas com os padrões. * * @return array */ function stoutwall_get_options() { return wp_parse_args((array) get_option('stoutwall_options', []), stoutwall_default_options()); } /** * Sanitiza um conjunto de opções (formulário, importação ou WP-CLI). * * Chaves ausentes em $input ficam desligadas, como acontece com checkboxes desmarcados. * * @param mixed $input Valores recebidos. * @return array */ function stoutwall_sanitize_options($input) { $input = is_array($input) ? $input : []; $sanitized = []; foreach (array_keys(stoutwall_default_switches()) as $key) { $sanitized[$key] = !empty($input[$key]) ? 1 : 0; } $source = isset($input['login_ip_source']) ? sanitize_key($input['login_ip_source']) : 'remote_addr'; $sanitized['login_ip_source'] = in_array($source, stoutwall_ip_sources(), true) ? $source : 'remote_addr'; return $sanitized; } /** * Retorna o IP do visitante de acordo com a fonte configurada. * * Cabeçalhos de proxy podem ser forjados por qualquer cliente; por isso o padrão é REMOTE_ADDR * e os outros só devem ser usados quando o site está atrás de um proxy que os define. * * @param string|null $source Fonte do IP (padrão: a configurada). * @return string IP válido ou string vazia. */ function stoutwall_client_ip($source = null) { if (null === $source) { $options = stoutwall_get_options(); $source = $options['login_ip_source']; } $headers = [ 'remote_addr' => 'REMOTE_ADDR', 'cf_connecting_ip' => 'HTTP_CF_CONNECTING_IP', 'x_real_ip' => 'HTTP_X_REAL_IP', 'x_forwarded_for' => 'HTTP_X_FORWARDED_FOR', ]; $header = isset($headers[$source]) ? $headers[$source] : 'REMOTE_ADDR'; $value = isset($_SERVER[$header]) ? sanitize_text_field(wp_unslash($_SERVER[$header])) : ''; // X-Forwarded-For é uma lista que o cliente pode iniciar com qualquer valor; só o último // item foi adicionado pelo proxy na frente do site, então é o único confiável. if ('x_forwarded_for' === $source) { $parts = explode(',', $value); $value = trim((string) end($parts)); } $ip = filter_var($value, FILTER_VALIDATE_IP) ? $value : ''; // Sem o cabeçalho configurado, cai para REMOTE_ADDR em vez de tratar todos como o mesmo IP. if ('' === $ip && 'remote_addr' !== $source) { return stoutwall_client_ip('remote_addr'); } /** * Filtra o IP usado pelo Stoutwall para identificar o visitante. * * @param string $ip IP detectado. * @param string $source Fonte configurada. */ return (string) apply_filters('stoutwall_client_ip', $ip, $source); } /** * Valida uma entrada da lista de IPs permitidos (IP ou faixa CIDR). * * @param string $entry Entrada, ex.: "203.0.113.7" ou "203.0.113.0/24". * @return string Entrada normalizada ou string vazia se for inválida. */ function stoutwall_normalize_ip_entry($entry) { $entry = trim((string) $entry); $parts = explode('/', $entry, 2); $ip = $parts[0]; if (!filter_var($ip, FILTER_VALIDATE_IP)) { return ''; } if (!isset($parts[1])) { return $ip; } $max = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? 32 : 128; if (!ctype_digit($parts[1]) || (int) $parts[1] > $max) { return ''; } return $ip . '/' . (int) $parts[1]; } /** * Lista de IPs que nunca são bloqueados pelo limite de tentativas de login. * * @return string[] */ function stoutwall_get_allowlist() { $list = get_option('stoutwall_login_allowlist', []); return is_array($list) ? array_values(array_filter(array_map('stoutwall_normalize_ip_entry', $list))) : []; } /** * Salva a lista de IPs permitidos, descartando entradas inválidas e repetidas. * * @param string[] $entries Entradas. * @return string[] Entradas inválidas (não salvas). */ function stoutwall_save_allowlist($entries) { $valid = []; $invalid = []; foreach ((array) $entries as $entry) { $entry = trim((string) $entry); if ('' === $entry) { continue; } $normalized = stoutwall_normalize_ip_entry($entry); if ('' === $normalized) { $invalid[] = $entry; } else { $valid[$normalized] = $normalized; } } update_option('stoutwall_login_allowlist', array_values($valid), false); return $invalid; } /** * Verifica se um IP está na lista (IP exato ou dentro de uma faixa CIDR). * * @param string $ip IP. * @param string[] $list Entradas normalizadas. * @return bool */ function stoutwall_ip_matches($ip, $list) { $packed = @inet_pton($ip); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if (false === $packed) { return false; } foreach ($list as $entry) { $parts = explode('/', $entry, 2); $subnet = @inet_pton($parts[0]); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if (false === $subnet || strlen($subnet) !== strlen($packed)) { continue; } $bits = isset($parts[1]) ? (int) $parts[1] : strlen($packed) * 8; $full = intdiv($bits, 8); $rest = $bits % 8; if (substr($packed, 0, $full) !== substr($subnet, 0, $full)) { continue; } if ($rest) { $mask = chr((0xff << (8 - $rest)) & 0xff); if ((ord($packed[$full]) & ord($mask)) !== (ord($subnet[$full]) & ord($mask))) { continue; } } return true; } return false; } /** * Migra a configuração da versão distribuída no GitHub como "ProtectMyWordPress". * * Copia as opções antigas (pmw_options) e remove o bloco antigo do .htaccess de uploads. * * @return void */ function stoutwall_migrate_legacy() { $legacy = get_option('pmw_options'); if (false !== $legacy) { if (false === get_option('stoutwall_options')) { update_option('stoutwall_options', wp_parse_args((array) $legacy, stoutwall_default_options())); } delete_option('pmw_options'); } Stoutwall_Security::removeLegacyHtaccessBlock(); } /** * Aplica as regras do .htaccess de uploads assim que as opções são salvas. * * @param mixed $old_value Valor anterior. * @param mixed $value Novo valor. * @return void */ function stoutwall_options_updated($old_value, $value) { Stoutwall_Security::writeUploadsHtaccess(!empty($value['prevent_uploads_code_execution'])); if (!empty($value['disable_directory_browsing'])) { Stoutwall_Security::writeUploadsIndex(); } } add_action('update_option_stoutwall_options', 'stoutwall_options_updated', 10, 2); /** * Função AJAX para checar o status dos módulos. * * @return void */ function stoutwall_ajax_check_status() { check_ajax_referer('stoutwall_check_status', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Access denied.', 'stoutwall'), 403); } wp_send_json_success(Stoutwall_Security::checkModulesStatus()); } add_action('wp_ajax_stoutwall_check_status', 'stoutwall_ajax_check_status'); /** * Função AJAX que roda a auditoria (inclui testes por HTTP, por isso não roda no carregamento). * * @return void */ function stoutwall_ajax_run_audit() { check_ajax_referer('stoutwall_run_audit', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Access denied.', 'stoutwall'), 403); } wp_send_json_success(Stoutwall_Audit::run(true)); } add_action('wp_ajax_stoutwall_run_audit', 'stoutwall_ajax_run_audit');