Файловый менеджер - Редактировать - /home/tuudkjt/globeasy/wp-includes/ID3/security.tar
Назад
index.php 0000777 00000000040 15251751331 0006370 0 ustar 00 <?php // You don't belong here. functions.php 0000777 00000051402 15251751331 0007301 0 ustar 00 <?php use RSSSL\Security\RSSSL_Htaccess_File_Manager; defined( 'ABSPATH' ) or die( ); // Htaccess marker constants const RSSSL_DISABLE_DIRECTORY_INDEXING_MARKER = 'Really Simple Security Disable directory indexing'; const RSSSL_NO_INDEX_LEGACY_MARKER = 'Really Simple Security No Index'; /** * Back-end available only */ if ( !function_exists('rsssl_do_fix')) { /** * Complete a fix for an issue, either user triggered, or automatic * * @param $fix * * @return void */ function rsssl_do_fix( $fix ) { if ( ! rsssl_user_can_manage() ) { return; } if ( ! rsssl_has_fix( $fix ) && function_exists( $fix ) ) { $completed[] = $fix; $fix(); $completed = get_option( 'rsssl_completed_fixes', [] ); $completed[] = $fix; update_option( 'rsssl_completed_fixes', $completed ); } else if ( $fix && ! function_exists( $fix ) ) { } } } if ( !function_exists('rsssl_has_fix')) { /** * Check if this has been fixed already * * @param $fix * * @return bool */ function rsssl_has_fix( $fix ) { $completed = get_option( 'rsssl_completed_fixes', [] ); if ( ! in_array( $fix, $completed ) ) { return false; } return true; } } if ( !function_exists('rsssl_admin_url')) { /** * Get admin url, adjusted for multisite * @param array $args //query args * @param string $path //hash slug for the settings pages (e.g. #dashboard) * @return string */ function rsssl_admin_url(array $args = [], string $path = ''): string { $url = is_multisite() ? network_admin_url('admin.php') : admin_url('admin.php'); $args = wp_parse_args($args, ['page' => 'really-simple-security']); return add_query_arg($args, $url) . $path; } } if ( !function_exists('rsssl_maybe_clear_transients')) { /** * If the corresponding setting has been changed, clear the test cache and re-run it. * * @return void */ function rsssl_maybe_clear_transients( $field_id, $field_value, $prev_value, $field_type ) { if ( $field_id === 'mixed_content_fixer' && $field_value ) { delete_transient( 'rsssl_mixed_content_fixer_detected' ); RSSSL()->admin->mixed_content_fixer_detected(); } //expire in five minutes $headers = get_transient('rsssl_can_use_curl_headers_check'); set_transient('rsssl_can_use_curl_headers_check', $headers, 5 * MINUTE_IN_SECONDS); //no change if ( $field_value === $prev_value ) { return; } if ( $field_id === 'disable_http_methods' ) { delete_option( 'rsssl_http_methods_allowed' ); rsssl_http_methods_allowed(); } if ( $field_id === 'xmlrpc' ) { delete_transient( 'rsssl_xmlrpc_allowed' ); rsssl_xmlrpc_allowed(); } if ( $field_id === 'disable_indexing' ) { delete_transient( 'rsssl_directory_indexing_status' ); rsssl_directory_indexing_allowed(); } if ( $field_id === 'block_code_execution_uploads' ) { delete_transient( 'rsssl_code_execution_allowed_status' ); rsssl_code_execution_allowed(); } if ( $field_id === 'hide_wordpress_version' ) { delete_option( 'rsssl_wp_version_detected' ); rsssl_src_contains_wp_version(); } if ( $field_id === 'rename_admin_user' ) { delete_transient('rsssl_admin_user_count'); rsssl_has_admin_user(); } } add_action( "rsssl_after_save_field", 'rsssl_maybe_clear_transients', 100, 4 ); } if ( !function_exists('rsssl_remove_htaccess_security_edits') ) { /** * Clean up on deactivation * * @param bool $clear_htaccess_redirect Whether to clear the htaccess redirect when deactivating * @return void */ function rsssl_remove_htaccess_security_edits( $clear_htaccess_redirect = false ) { if ( ! rsssl_user_can_manage() ) { return; } if ( ! rsssl_uses_htaccess() ) { return; } $htaccess_file = RSSSL()->admin->htaccess_file(); if ( ! file_exists( $htaccess_file ) ) { return; } $start = "\n" . '#Begin Really Simple Security'; $end = '#End Really Simple Security' . "\n"; $pattern = '/'.$start.'(.*?)'.$end.'/is'; /** * htaccess in uploads dir */ $upload_dir = wp_get_upload_dir(); $htaccess_file_uploads = trailingslashit( $upload_dir['basedir']).'.htaccess'; $content_htaccess_uploads = is_file($htaccess_file_uploads ) ? file_get_contents($htaccess_file_uploads) : ''; if (preg_match($pattern, $content_htaccess_uploads) && is_writable( $htaccess_file_uploads )) { $content_htaccess_uploads = preg_replace($pattern, "", $content_htaccess_uploads); file_put_contents( $htaccess_file_uploads, $content_htaccess_uploads, LOCK_EX ); } // Uses the new conversion of the htaccess file manager $root_htaccess_file = RSSSL()->admin->htaccess_file(); $root_manager = RSSSL_Htaccess_File_Manager::get_instance(); /* * This is the root .htaccess file, which is used for security rules. * We will clear the security rules from this file. * This is done by clearing the rules that were added by the plugin. * The rules are identified by their marker, which is a comment line in the .htaccess file. * The marker is used to identify the rules that were added by the plugin. * * note: Only this is for the root .htaccess file, not the uploads .htaccess file. */ if ( ! $root_manager->validate_htaccess_file_path() ) { return; } // Only clear redirect rules if explicitly requested if ( $clear_htaccess_redirect ) { // Clear redirect rules block $root_manager->clear_rule( 'Really Simple Security Redirect', 'clear redirect 1' ); //Legacy rules $root_manager->clear_legacy_rule( 'Really Simple Security Redirect' ); // Clear any remaining security rules block $root_manager->clear_legacy_rule( 'Really Simple Security' ); // Clear disable directory indexing block $root_manager->clear_rule( RSSSL_DISABLE_DIRECTORY_INDEXING_MARKER, 'clear disable directory indexing' ); // Clear legacy Really Simple SSL block $root_manager->clear_legacy_rule( 'rlrssslReallySimpleSSL' ); } } } /** * Wrap the security headers */ if ( ! function_exists('rsssl_wrap_htaccess' ) ) { function rsssl_wrap_htaccess() { if ( ! rsssl_htaccess_should_wrap() ) { return; } update_option( 'rsssl_htaccess_should_wrap', true, false ); rsssl_htaccess_clear_errors(); rsssl_handle_uploads_htaccess(); rsssl_handle_root_htaccess(); rsssl_htaccess_finalize(); } add_action('admin_init', 'rsssl_wrap_htaccess' ); add_action('rsssl_after_saved_fields', 'rsssl_wrap_htaccess', 30); } /** * Check whether we should wrap htaccess. * * @return bool */ function rsssl_htaccess_should_wrap(): bool { if ( ! rsssl_user_can_manage() || ! rsssl_uses_htaccess() ) { return false; } if ( rsssl_get_option('do_not_edit_htaccess') ) { delete_site_option('rsssl_htaccess_error'); delete_site_option('rsssl_htaccess_rules'); return false; } if ( get_option('rsssl_updating_htaccess') ) { return false; } return true; } /** * Finalize htaccess wrapping by removing the updating flag. */ function rsssl_htaccess_finalize(): void { delete_option('rsssl_updating_htaccess'); } /** * Handle root directory .htaccess wrapping. */ function rsssl_handle_root_htaccess(): void { $rules = apply_filters( 'rsssl_htaccess_security_rules', [] ); $htaccess_file = RSSSL()->admin->htaccess_file(); // If there are no rules at all, nothing to do (or record an error) if ( empty( $rules ) ) { delete_site_option( 'rsssl_htaccess_error' ); delete_site_option( 'rsssl_htaccess_rules' ); return; } // If file doesn’t exist yet, record that and cache the rules for later if ( ! is_file( $htaccess_file ) ) { update_site_option( 'rsssl_htaccess_error', 'not-exists' ); update_site_option( 'rsssl_htaccess_rules', implode( '', array_column( $rules, 'rules' ) ) ); return; } if ( is_file( $htaccess_file ) ) { // Main path: file exists and we have rules $manager = new RSSSL_Htaccess_File_Manager(); $manager->set_htaccess_file_path( $htaccess_file ); $definition = ''; $no_index_definition = ''; // 1) Drop any legacy blocks rsssl_clear_legacy_rules( $manager ); // 2) Build the new redirect‐rules block foreach ( $rules as $idx => $rule ) { if ( isset( $rule['identifier'] ) && $rule['identifier'] === 'RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1' ) { // removing the identifier from the rule, as it is not used in the new htaccess file manager unset( $rule['identifier'] ); // 2.2) Add the redirect block $definition = rsssl_build_redirect_block( $manager, $rule ); // remove this rule unset( $rules[ $idx ] ); break; // stop after first match } } foreach ( $rules as $idx => $rule ) { if ( isset( $rule['identifier'] ) && $rule['identifier'] === 'Options -Indexes' ) { // removing the identifier from the rule, as it is not used in the new htaccess file manager unset( $rule['identifier'] ); // 2.1) Add the disable directory indexing block $no_index_definition = rsssl_build_disable_indexing_block( $manager ); // remove this rule unset( $rules[ $idx ] ); break; // stop after first match } } // 3) If the file isn’t writable, record an error; otherwise write it if ( ! is_writable( $htaccess_file ) ) { update_site_option( 'rsssl_htaccess_error', 'not-writable' ); if (is_array($definition) && !empty($definition['lines'])) { update_site_option( 'rsssl_htaccess_rules', implode( "\n", $definition['lines'])); } return; } delete_site_option( 'rsssl_htaccess_error' ); delete_site_option( 'rsssl_htaccess_rules' ); if( !empty( $no_index_definition['lines'] ) ) { // If we have a no-indexing block, write it first $manager->write_rule( $no_index_definition, 'Writing no index block' ); } elseif( ! rsssl_get_option( 'disable_indexing', false ) ) { // If we don't have a disable directory indexing block, clear it $manager->clear_rule( RSSSL_DISABLE_DIRECTORY_INDEXING_MARKER, 'clear disable directory indexing' ); } // // 4) Write the redirect block but only if it’s not empty if ( ! empty( $definition['lines'] ) ) { $manager->write_rule( $definition, 'Writing redirect block' ); } if ( rsssl_get_option('redirect') !== 'htaccess' ) { $manager->clear_rule( 'Really Simple Security Redirect', 'clear redirect 2 and value of config:' . rsssl_get_option('redirect') ); } } } /** * Build the redirect block for the .htaccess file. * * @param RSSSL_Htaccess_File_Manager $m * @param array $lines the lines for the redirect block. * * @return array */ function rsssl_build_redirect_block( RSSSL_Htaccess_File_Manager $m, array $lines = [] ): array { if ( empty($lines) ) { return [ 'marker' => 'Really Simple Security Redirect', 'lines' => [], ]; } // In case legacy markers are present, skip the rule. They should be // cleared before this function is called. $legacyMarkerPresent = $m->are_markers_present([ '#BEGIN Really Simple Security Redirect', '#END Really Simple Security Redirect', ]); return [ 'marker' => 'Really Simple Security Redirect', 'lines' => $lines, ]; } /** * Build the disable directory indexing block for the .htaccess file. * * @param RSSSL_Htaccess_File_Manager $m * @return array */ function rsssl_build_disable_indexing_block( RSSSL_Htaccess_File_Manager $m ): array { $content = $m->get_htaccess_content() ?: ''; $no_index = 'Options -Indexes'; if ( strpos( $content, $no_index ) !== false ) { return []; } return [ 'marker' => RSSSL_DISABLE_DIRECTORY_INDEXING_MARKER, 'lines' => [ '# Disable directory indexing to prevent listing of directory contents', $no_index ], ]; } /** * Handle uploads directory .htaccess wrapping. * TODO also needs to convert to the new file manager. */ function rsssl_handle_uploads_htaccess(): void { $start = '#Begin Really Simple Security'; $end = "\n" . '#End Really Simple Security' . "\n"; $pattern_content = '/' . preg_quote( $start, '/' ) . '(.*?)' . preg_quote( $end, '/' ) . '/is'; $pattern = '/' . preg_quote( $start, '/' ) . '.*?' . preg_quote( $end, '/' ) . '/is'; $rules_uploads = apply_filters( 'rsssl_htaccess_security_rules_uploads', [] ); $upload_dir = wp_get_upload_dir(); $htaccess_uploads = trailingslashit( $upload_dir['basedir'] ) . '.htaccess'; if ( ! is_file( $htaccess_uploads ) && count( $rules_uploads ) > 0 ) { if ( is_writable( trailingslashit( $upload_dir['basedir'] ) ) ) { file_put_contents( $htaccess_uploads, '', LOCK_EX ); } else { update_site_option( 'rsssl_uploads_htaccess_error', 'not-writable' ); $rules_uploads_result = implode( '', array_column( $rules_uploads, 'rules' ) ); update_site_option( 'rsssl_uploads_htaccess_rules', $rules_uploads_result ); } } if ( is_file( $htaccess_uploads ) ) { $content = file_get_contents( $htaccess_uploads ); preg_match( $pattern_content, $content, $matches ); if ( ( ! empty( $matches[1] ) && empty( $rules_uploads ) ) || ! empty( $rules_uploads ) ) { $rules_uploads_result = ''; foreach ( $rules_uploads as $rule ) { if ( strpos( $content, $rule['identifier'] ) !== false && ! preg_match( '/' . preg_quote( $start, '/' ) . '.*?(' . preg_quote( $rule['identifier'], '/' ) . ').*?' . preg_quote( $end, '/' ) . '/is', $content ) ) { continue; } $rules_uploads_result .= $rule['rules']; } $has_block = preg_match( '/#Begin Really Simple Security.*?#End Really Simple Security/is', $content ); if ( ! empty( $rules_uploads_result ) || $has_block ) { if ( ! is_file( $htaccess_uploads ) ) { file_put_contents( $htaccess_uploads, '', LOCK_EX ); } $new_block = empty( $rules_uploads_result ) ? '' : $start . $rules_uploads_result . $end; if ( ! is_writable( $htaccess_uploads ) ) { update_site_option( 'rsssl_uploads_htaccess_error', 'not-writable' ); update_site_option( 'rsssl_uploads_htaccess_rules', $rules_uploads_result ); } else { delete_site_option( 'rsssl_uploads_htaccess_error' ); delete_site_option( 'rsssl_uploads_htaccess_rules' ); $cleaned = preg_replace( $pattern, '', $content ); $new = $cleaned . "\n" . $new_block; $new = preg_replace( "/\n{3,}/", "\n\n", $new ); if ( file_get_contents( $htaccess_uploads ) !== $new ) { file_put_contents( $htaccess_uploads, $new, LOCK_EX ); } } } } } } /** * Clear any stored htaccess errors/options. */ function rsssl_htaccess_clear_errors(): void { delete_site_option('rsssl_htaccess_error'); delete_site_option('rsssl_htaccess_rules'); delete_site_option('rsssl_uploads_htaccess_error'); delete_site_option('rsssl_uploads_htaccess_rules'); } function rsssl_clear_legacy_rules( RSSSL_Htaccess_File_Manager $m ) { foreach ( [ 'rlrssslReallySimpleSSL', 'Really Simple Security', 'Really Simple Security Redirect', ] as $marker ) { $m->clear_legacy_rule( $marker ); } } /** * Store warning blocks for later use in the mailer * * @param array $changed_fields * * @return void */ function rsssl_gather_warning_blocks_for_mail( array $changed_fields ){ if (!rsssl_user_can_manage() ) { return; } if ( !rsssl_get_option('send_notifications_email') ) { return; } $fields = array_filter($changed_fields, static function($field) { // Check if email_condition exists and call the function, else assume true if ( !isset($field['email']['condition']) ) { $email_condition_result = true; } else if (is_array($field['email']['condition'])) { //rsssl option check $fieldname = array_key_first($field['email']['condition']); $value = $field['email']['condition'][$fieldname]; $email_condition_result = rsssl_get_option($fieldname) === $value; } else { //function check $function = $field['email']['condition']; $email_condition_result = function_exists($function) && $function(); } return isset($field['email']['message']) && $field['value'] && $email_condition_result; }); if ( count($fields)===0 ) { return; } $current_fields = get_option('rsssl_email_warning_fields', []); //if it's empty, we start counting time. 30 mins later we send a mail. update_option('rsssl_email_warning_fields_saved', time(), false ); $current_ids = array_column($current_fields, 'id'); foreach ($fields as $field){ if ( !in_array( $field['id'], $current_ids, true ) ) { $current_fields[] = $field; } } update_option('rsssl_email_warning_fields', $current_fields, false); } add_action('rsssl_after_saved_fields', 'rsssl_gather_warning_blocks_for_mail', 40); /** * Check if server uses .htaccess * @return bool */ function rsssl_uses_htaccess() { //when using WP CLI, the get_server check does not work, so we assume .htaccess is being used //and rely on the file exists check to catch if not. if ( defined( 'WP_CLI' ) && WP_CLI ) { return true; } return rsssl_get_server() === 'apache' || rsssl_get_server() === 'litespeed'; } /** * Get htaccess status * @return string | bool */ function rsssl_htaccess_status(){ if ( empty(get_site_option('rsssl_htaccess_rules','')) ) { return false; } return get_site_option('rsssl_htaccess_error'); } /** * Get htaccess status * @return string | bool */ function rsssl_uploads_htaccess_status(){ if ( empty(get_site_option('rsssl_uploads_htaccess_rules','')) ) { return false; } return get_site_option('rsssl_uploads_htaccess_error'); } /** * @return string|null * Get the wp-config.php path */ function rsssl_find_wp_config_path() { if ( ! rsssl_user_can_manage() ) { return null; } // Allow the wp-config.php path to be overridden via a filter. $filtered_path = apply_filters( 'rsssl_wpconfig_path', '' ); // If a filtered path is provided, validate it. if ( ! empty( $filtered_path ) ) { $directory = dirname( $filtered_path ); // Ensure the directory exists before checking for the file. if ( is_dir( $directory ) && file_exists( $filtered_path ) ) { return $filtered_path; } } // Limit number of iterations to 10 $i = 0; $dir = __DIR__; do { $i ++; if ( file_exists( $dir . "/wp-config.php" ) ) { return $dir . "/wp-config.php"; } } while ( ( $dir = realpath( "$dir/.." ) ) && ( $i < 10 ) ); return null; } /** * Returns the server type of the plugin user. * * @return string|bool server type the user is using of false if undetectable. */ function rsssl_get_server() { //Allows to override server authentication for testing or other reasons. if ( defined( 'RSSSL_SERVER_OVERRIDE' ) ) { return RSSSL_SERVER_OVERRIDE; } $server_raw = strtolower( htmlspecialchars( $_SERVER['SERVER_SOFTWARE'], ENT_QUOTES | ENT_HTML5 ) ); //figure out what server they're using if ( strpos( $server_raw, 'apache' ) !== false ) { return 'apache'; } elseif ( strpos( $server_raw, 'nginx' ) !== false ) { return 'nginx'; } elseif ( strpos( $server_raw, 'litespeed' ) !== false ) { return 'litespeed'; } else { //unsupported server return false; } } /** * @return string * Generate a random prefix */ function rsssl_generate_random_string($length) { $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $randomString = ''; for ( $i = 0; $i < $length; $i++ ) { $index = rand(0, strlen($characters) - 1); $randomString .= $characters[$index]; } return $randomString; } /** * @return string * * Get users as string to display */ function rsssl_list_users_where_display_name_is_login_name() { if ( !rsssl_user_can_manage() ) { return ''; } $users = rsssl_get_users_where_display_name_is_login( true ); if ( is_array( $users ) ) { $ext = count($users)>=10 ? '...' : ''; $users = array_slice($users, 0, 10); return implode( ', ', $users ).$ext; } return ''; } /** * Check if user e-mail is verified * @return bool */ function rsssl_is_email_verified() { $verificationStatus = get_option('rsssl_email_verification_status'); if (rsssl_user_can_manage() && $verificationStatus == 'completed') { return true; } // User cannot manage or status is ['started', 'email_changed'] return false; } function rsssl_remove_prefix_from_version($version) { return preg_replace('/^[^\d]*(?=\d)/', '', $version); } function rsssl_version_compare($version, $compare_to, $operator = null) { $version = rsssl_remove_prefix_from_version($version); $compare_to = rsssl_remove_prefix_from_version($compare_to); return version_compare($version, $compare_to, $operator); } function rsssl_maybe_disable_404_blocking() { $option_value = get_option( 'rsssl_homepage_contains_404_resources', false ); // Explicitly check for boolean true or string "true" return $option_value === true || $option_value === "true"; } function rsssl_lock_file_exists() { if ( file_exists( trailingslashit( WP_CONTENT_DIR ) . 'rsssl-safe-mode.lock' ) ) { return true; } return false; } security.php 0000777 00000003415 15251751331 0007141 0 ustar 00 <?php use RSSSL\Security\RSSSL_Htaccess_File_Manager; defined('ABSPATH') or die(); class REALLY_SIMPLE_SECURITY { private static $instance; public $firewall_manager; public $hardening; /** * Components array, so we can access singleton classes which are dynamically added, from anywhere. * @var */ public $components; private function __construct() { if (!defined('RSSSL_SAFE_MODE') && file_exists(trailingslashit(WP_CONTENT_DIR) . 'rsssl-safe-mode.lock')) { define('RSSSL_SAFE_MODE', true); } } public static function instance() { if (!isset(self::$instance) && !(self::$instance instanceof REALLY_SIMPLE_SECURITY)) { self::$instance = new REALLY_SIMPLE_SECURITY; self::$instance->includes(); if ( rsssl_admin_logged_in() ) { $htaccessFileManager = new RSSSL_Htaccess_File_Manager(); self::$instance->firewall_manager = new rsssl_firewall_manager($htaccessFileManager); self::$instance->hardening = new rsssl_hardening(); } } return self::$instance; } private function includes() { $path = rsssl_path.'security/'; require_once( $path . 'integrations.php' ); require_once( $path . 'hardening.php' ); require_once( $path . 'cron.php' ); require_once( $path . 'includes/check404/class-rsssl-simple-404-interceptor.php' ); /** * Load only on back-end */ if ( rsssl_admin_logged_in() ) { require_once( $path . 'functions.php' ); require_once( $path . 'deactivate-integration.php' ); require_once( $path . 'firewall-manager.php' ); require_once( $path . 'tests.php' ); require_once( $path . 'notices.php' ); require_once( $path . 'sync-settings.php' ); } } } function RSSSL_SECURITY() { return REALLY_SIMPLE_SECURITY::instance(); } add_action('plugins_loaded', 'RSSSL_SECURITY', 9); hardening.php 0000777 00000004324 15251751331 0007231 0 ustar 00 <?php defined('ABSPATH') or die(); class rsssl_hardening { private static $_this; public $risk_naming; function __construct() { if (isset(self::$_this)) wp_die(sprintf(__('%s is a singleton class and you cannot create a second instance.', 'really-simple-ssl'), get_class($this))); add_filter( 'rsssl_do_action', array($this, 'hardening_data'), 10, 3 ); add_action("admin_init", array($this, "load_translations")); self::$_this = $this; } public function load_translations(){ $this->risk_naming = [ 'l' => __('low-risk', 'really-simple-ssl'), 'm' => __('medium-risk', 'really-simple-ssl'), 'h' => __('high-risk', 'really-simple-ssl'), 'c' => __('critical', 'really-simple-ssl'), ]; } function hardening_data( array $response, string $action, $data ): array { if ( ! rsssl_user_can_manage() ) { return $response; } if ($action === 'hardening_data') { $response = $this->get_stats( $data ); } return $response; } static function this() { return self::$_this; } /* Public Section 2: DataGathering */ /** * @param $data * * @return array */ public function get_stats($data): array { if ( ! rsssl_user_can_manage() ) { return []; } $vulEnabled = rsssl_get_option('enable_vulnerability_scanner'); //now we fetch all plugins that have an update available. $stats = [ 'updates' => $this->getAllUpdatesCount(), 'lastChecked' => time(), 'riskNaming' => $this->risk_naming, 'vulEnabled' => $vulEnabled, ]; $repsonse = [ "request_success" => true, 'data' => apply_filters('rsssl_vulnerability_data', $stats), ]; return $repsonse; } /** * Gets the count of all available updates for core, plugins, and themes. * * @return int The count of all available updates. */ public function getAllUpdatesCount(): int { $updatesData = wp_get_update_data(); // Checks if the 'counts' key exists in the array and it's an array itself. if (isset($updatesData['counts']) && is_array($updatesData['counts'])) { //we only want core, plugins and themes. $updatesCounts = array_slice($updatesData['counts'], 0, 3); return array_sum($updatesCounts); } // Fallback return in case there's no 'counts' key or it's not an array. return 0; } } tests/code-execution.php 0000777 00000000233 15251751331 0011342 0 ustar 00 <?php /** * Test file for Really Simple Security to check if uploads directory has code execution permissions * */ echo "RSSSL CODE EXECUTION MARKER"; tests/index.php 0000777 00000000043 15251751331 0007535 0 ustar 00 <?php // You don't belong here. ?> notices.php 0000777 00000014036 15251751331 0006737 0 ustar 00 <?php defined( 'ABSPATH' ) or die(); /** * Convert htaccess rules to html friendly layout * * @param string $code * * @return string */ function rsssl_parse_htaccess_to_html( string $code): string { if ( strpos($code, "\n")===0 ) { $code = preg_replace('/\n/', '', $code, 1); } //split into linebreak separated array, so we can run esc_html on the result $code = preg_replace('/\n/', '--br--', $code, 1); $code = preg_replace('/<br>/', '--br--', $code, 1); $code_arr = explode('--br--', $code); $code_arr = array_map('esc_html', $code_arr); $code = implode('<br>', $code_arr); return '<br><code>' . $code . '</code><br>'; } function rsssl_general_security_notices( $notices ) { $code = rsssl_parse_htaccess_to_html( get_site_option( 'rsssl_htaccess_rules', '' ) ); $uploads_code = rsssl_parse_htaccess_to_html( get_site_option( 'rsssl_uploads_htaccess_rules', '' ) ); $open_hardening_count = rsssl_count_open_hardening_features(); $notices['htaccess_status'] = array( 'callback' => 'rsssl_htaccess_status', 'score' => 5, 'output' => array( 'not-writable' => array( 'title' => __( ".htaccess not writable", "really-simple-ssl" ), 'msg' => __( "An option that requires the .htaccess file is enabled, but the file is not writable.", "really-simple-ssl" ) . ' ' . __( "Please add the following lines to your .htaccess, or set it to writable:", "really-simple-ssl" ) . $code, 'icon' => 'warning', 'dismissible' => true, 'plusone' => true, 'url' => 'manual/editing-htaccess/', ), 'not-exists' => array( 'title' => __( ".htaccess does not exist", "really-simple-ssl" ), 'msg' => __( "An option that requires the .htaccess file is enabled, but the file does not exist.", "really-simple-ssl" ) . ' ' . __( "Please add the following lines to your .htaccess, or set it to writable:", "really-simple-ssl" ) . $code, 'icon' => 'warning', 'dismissible' => true, 'plusone' => true, 'url' => 'manual/editing-htaccess/', ), ), 'show_with_options' => [ 'disable_indexing', 'redirect' ] ); $notices['htaccess_status_uploads'] = array( 'callback' => 'rsssl_uploads_htaccess_status', 'score' => 5, 'output' => array( 'not-writable' => array( 'title' => __( ".htaccess in uploads not writable", "really-simple-ssl" ), 'msg' => __( "An option that requires the .htaccess file in the uploads directory is enabled, but the file is not writable.", "really-simple-ssl" ) . ' ' . __( "Please add the following lines to your .htaccess, or set it to writable:", "really-simple-ssl" ) . $uploads_code, 'icon' => 'warning', 'dismissible' => true, 'plusone' => true, 'url' => 'manual/editing-htaccess/', ), ), 'show_with_options' => [ 'block_code_execution_uploads', ] ); $notices['display_name_is_login_exists'] = array( 'condition' => [ 'rsssl_get_users_where_display_name_is_login' ], 'callback' => '_true_', 'score' => 5, 'output' => array( 'true' => array( 'url' => 'manual/login-and-display-names-should-be-different-for-wordpress/', 'msg' => __( "We have detected administrator roles where the login and display names are the same.", "really-simple-ssl" ) . " <b>" . rsssl_list_users_where_display_name_is_login_name() . "</b>", 'icon' => 'open', 'dismissible' => true, ), ), ); $notices['new_username_empty'] = array( 'condition' => [ 'rsssl_has_admin_user', 'option_rename_admin_user', 'NOT rsssl_new_username_valid' ], 'callback' => '_true_', 'score' => 5, 'output' => array( 'true' => array( 'highlight_field_id' => 'rename_admin_user', 'title' => __( "Username", "really-simple-ssl" ), 'msg' => __( "Rename admin user enabled: Please choose a new username of at least 3 characters, which is not in use yet.", "really-simple-ssl" ), 'icon' => 'warning', 'dismissible' => true, ), ), 'show_with_options' => [ 'new_admin_user_login', ], ); $notices['enable_vulnerability_scanner'] = array( 'callback' => 'option_enable_vulnerability_scanner', 'score' => 5, 'output' => array( 'false' => array( 'highlight_field_id' => 'enable_vulnerability_scanner', 'msg' => __( "Enable the Vulnerability scan to detect possible vulnerabilities.", 'really-simple-ssl' ), 'icon' => 'open', 'admin_notice' => false, 'dismissible' => true, 'plusone' => false, ), 'true' => array( 'msg' => __( "Vulnerability scanning is enabled.", 'really-simple-ssl' ), 'icon' => 'success', ), ), ); $notices['count_open_hardening_features'] = array( 'callback' => 'rsssl_has_open_hardening_features', 'score' => 5, 'output' => array( 'true' => array( 'highlight_field_id' => 'disable_anyone_can_register', 'msg' => sprintf( _n( "You have %s open hardening feature.", "You have %s open hardening features.", $open_hardening_count, "really-simple-ssl" ), $open_hardening_count ), 'icon' => 'open', 'dismissible' => true, ), 'false' => array( 'msg' => __( "All recommended hardening features enabled.", "really-simple-ssl" ), 'icon' => 'success', ), ), ); $notices['lock_file_exists'] = array( 'callback' => 'rsssl_lock_file_exists', 'score' => 5, 'output' => array( 'true' => array( 'msg' => __( 'The Firewall, LLA and 2FA are currently inactive, as you have activated Safe Mode with the rsssl-safe-mode.lock file. Remove the file from your /wp-content folder after you have finished debugging.', 'really-simple-ssl' ), 'icon' => 'warning', ), ), ); return $notices; } add_filter('rsssl_notices', 'rsssl_general_security_notices'); wordpress/two-fa/class-rsssl-two-fa-status.php 0000777 00000006574 15251751331 0015515 0 ustar 00 <?php /** * Two-Factor Authentication. * Status class. * * @package REALLY_SIMPLE_SSL */ namespace RSSSL\Security\WordPress\Two_Fa; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Provider_Loader; use RSSSL\Security\WordPress\Two_Fa\Traits\Rsssl_Two_Fa_Helper; use WP_User; /** * Class Rsssl_Two_Fa_Status * * Represents the two-factor authentication status. * * @package REALLY_SIMPLE_SSL */ class Rsssl_Two_Fa_Status { use Rsssl_Two_Fa_Helper; public const STATUSES = array( 'disabled', 'open', 'active' ); // This is a list of all available statuses. /** * Get the status of two-factor authentication for a user. * * @param WP_User $user (optional) The user for which to retrieve the status. Defaults to current user. * * @return array An associative array where the method names are the keys and the status values are the values. * The status can be one of the following: 'disabled' if the method is disabled for the user, * 'enabled' if the method is enabled for the user, or 'unknown' if the status could not be determined. */ public static function get_user_two_fa_status( WP_User $user ): array { $loader = Rsssl_Provider_Loader::get_loader(); $two_fa_providers = $loader::TWO_FA_PROVIDERS; // Assume this function returns all available methods. $statuses = array(); foreach ( $two_fa_providers as $two_fa_provider ) { $status = self::get_user_status( $two_fa_provider, $user->ID ); $statuses[ $two_fa_provider ] = $status ?: 'disabled'; } return $statuses; } /** * Get the user's two-factor authentication status. * * @param string $method The authentication method used by the user. * @param int $user_id The ID of the user. * * @return string The user's two-factor authentication status (enabled or disabled). */ public static function get_user_status( string $method, int $user_id ): string { $activated = $method === 'email' ? '_email' : '_' . self::sanitize_method( $method ); // Check the roles per method if they are enabled. $enabled_roles = rsssl_get_option( 'two_fa_enabled_roles'.$activated, array()); if ( empty( $enabled_roles ) && self::is_user_role_enabled( $user_id, $enabled_roles )) { return 'disabled'; } $status = get_user_meta( $user_id, "rsssl_two_fa_status_$method", true ); return self::sanitize_status( $status ); } /** * Delete two-factor authentication metadata for a user. * * @return void */ public static function delete_two_fa_meta(int $user_id ): void { // Reset the user based on the providers list. foreach ( Rsssl_Provider_Loader::get_loader()::available_providers() as $provider ) { $provider::reset_meta_data( $user_id ); update_user_meta($user_id, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s')); } } /** * Checks if a user has any of the enabled roles. * * @param int $user_id The user ID. * @param array $enabled_roles The enabled roles to check against. * * @return bool Returns true if the user has any of the enabled roles, false otherwise. */ private static function is_user_role_enabled( int $user_id, array $enabled_roles ):bool { $user = get_userdata( $user_id ); if ( ! $user ) { return false; } foreach ( $user->roles as $role ) { if ( in_array( $role, $enabled_roles, true ) ) { return true; } } return false; } } wordpress/two-fa/function-login-footer.php 0000777 00000003561 15251751331 0014750 0 ustar 00 <?php /** * Extracted from wp-login.php since that file also loads WP core which already have. * * @package REALLY_SIMPLE_SSL */ /** * Outputs the footer for the login page. * * @param string $input_id Which input to auto-focus. * * @global bool|string $interim_login Whether interim login modal is being displayed. String 'success' * upon successful login. * * @since 3.1.0 */ function login_footer( string $input_id = '' ) { global $interim_login; // Don't allow interim logins to navigate away from the page. if ( ! $interim_login ) { ?> <p id="backtoblog"> <?php $html_link = sprintf( '<a href="%s">%s</a>', esc_url( home_url( '/' ) ), sprintf( /* translators: %s: Site title. */ _x( '← Go to %s', 'site' ), get_bloginfo( 'title', 'display' ) ) ); /** * Filter the "Go to site" link displayed in the login page footer. * * @since 5.7.0 * * @param string $link HTML link to the home URL of the current site. */ echo esc_url( apply_filters( 'login_site_html_link', $html_link ) ); ?> </p> <?php the_privacy_policy_link( '<div class="privacy-policy-page-link">', '</div>' ); } ?> </div><?php // End of <div id="login">. ?> <?php if ( ! empty( $input_id ) ) { ?> <script type="text/javascript"> try{document.getElementById('<?php echo esc_html( $input_id ); ?>').focus();}catch(e){} if(typeof wpOnload==='function')wpOnload(); </script> <?php } /** * Fires in the login page footer. * * @since 3.1.0 */ do_action( 'login_footer' ); ?> <div class="clear"></div> </body> </html> <?php } /** * Outputs the JavaScript to handle the form shaking on the login page. * * @since 3.0.0 */ function wp_shake_js() { ?> <script type="text/javascript"> document.querySelector('form').classList.add('shake'); </script> <?php } wordpress/two-fa/controllers/class-rsssl-abstract-controller.php 0000777 00000007433 15251751331 0021324 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Controllers; use Exception; use ReflectionClass; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Request_Parameters; use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Authentication; use RSSSL\Security\WordPress\Two_Fa\Traits\Rsssl_Args_Builder; use RSSSL\Security\WordPress\Two_Fa\Traits\Rsssl_Two_Fa_Helper; use WP_REST_Request; use WP_User; abstract class Rsssl_Abstract_Controller { use Rsssl_Args_Builder; use Rsssl_Two_Fa_Helper; /** * The default HTTP method for the routes. * Child classes can override this constant if necessary. */ protected const METHOD = 'POST'; /** * The base route for all API endpoints. * Child classes should specify the route for their own context. */ protected const FEATURE_ROUTE = '/two-fa'; /** * The namespace for the API routes. * This will be dynamically set in the constructor. */ protected string $namespace; /** * Constructor to set the namespace and initialize the API routes. * Child classes should pass their own namespace and version. * * @param string $namespace The base namespace for the API. * @param string $version The version of the API. E.g., 'v1'. */ public function __construct(string $namespace, string $version, string $featureVersion) { $this->namespace = strtolower($namespace) . '/' .strtolower( $version ) . self::FEATURE_ROUTE . '/' . strtolower( $featureVersion ); $reflect = new ReflectionClass($this); if (!$reflect->isFinal()) { wp_die('Subclasses of Rsssl_Abstract_Controller must be declared as final.'); } } /** * Abstract method to register API routes. * Must be implemented by subclasses. * * @return void */ abstract public function register_api_routes(): void; /** * Registers a REST API route. * * @param string $namespace The namespace for the route. * @param string $method The HTTP method for the route (e.g., 'POST', 'GET'). * @param string $route The route endpoint. * @param callable $callback The callback function to handle the request. * @param callable|null $permission_callback The permission callback function or true to allow all requests. * @param array $args Optional. The arguments for the route. * * @return void * @throws Exception */ protected function route( string $namespace, string $method, string $route, callable $callback, ?callable $permission_callback = null, array $args = array() ): void { if ($permission_callback === null) { $permission_callback = array($this, 'permission_check'); } register_rest_route($namespace, $route, array( 'methods' => $method, 'permission_callback' => $permission_callback, 'callback' => $callback, 'args' => $args, )); } /** * Checks if the user is logged in and has the correct nonce. * * * @return bool */ public function permission_check(WP_REST_Request $request):bool { $parameters = new Rsssl_Request_Parameters( $request ); return $this->verify_hashed_user_id( $parameters->user_id, $parameters->login_nonce ); } /** * Verifies a login nonce, gets user by the user id, and returns an error response if any steps fail. * * @throws Exception */ public function check_login_and_get_user( int $user_id, string $login_nonce ): WP_User { if ( ! Rsssl_Two_Fa_Authentication::verify_login_nonce( $user_id, $login_nonce ) ) { // We throw an error wp_die(); } /** * Get the user by the user ID. * * @var WP_User $user */ $user = get_user_by('id', $user_id); if (!$user) { throw new Exception('User not found'); } return $user; } } wordpress/two-fa/controllers/class-rsssl-email-controller.php 0000777 00000030705 15251751331 0020606 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Controllers; use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Settings; use WP_Error; use Exception; use RSSSL\Pro\Security\WordPress\Limitlogin\Rsssl_IP_Fetcher; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Email; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Request_Parameters; use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Authentication; use WP_REST_Request; use WP_REST_Response; final class Rsssl_Email_Controller extends Rsssl_Abstract_Controller { protected const METHOD = 'POST'; protected const FEATURE_ROUTE = '/two-fa'; protected string $namespace; public function __construct($namespace, $version, $featureVersion) { parent::__construct($namespace, $version, $featureVersion); add_action('rest_api_init', array($this, 'register_api_routes')); } /** * Registers the REST API routes for the email controller. * * @return void * @throws Exception */ public function register_api_routes(): void { $this->route($this->namespace, self::METHOD, 'save_default_method_email', array($this, 'set_as_email'), null, $this->build_args(array('user_id', 'login_nonce', 'provider'), array('redirect_to')) ); $this->route($this->namespace, self::METHOD, 'save_default_method_email_profile', array($this, 'set_profile_email'), null, $this->build_args(array('user_id', 'login_nonce', 'provider'), array('redirect_to')) ); $this->route($this->namespace, self::METHOD, 'validate_email_setup', array($this, 'validate_email_setup'), array($this, 'permission_callback_login_actions'), $this->build_args(array('provider', 'user_id', 'login_nonce', 'token'), array('redirect_to')) ); $this->route($this->namespace, self::METHOD, 'resend_email_code', array($this, 'resend_email_code'), array($this, 'permission_callback_login_actions'), $this->build_args(array('user_id', 'login_nonce', 'provider'), array('profile')) ); } ############################### # All callback functions here # ############################### /** * Sets the profile email for a user. * * @param WP_REST_Request $request The REST request object. * * @return WP_REST_Response The REST response object. */ public function set_profile_email(WP_REST_Request $request): WP_REST_Response { $parameters = new Rsssl_Request_Parameters($request); try { $user = $this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce); } catch (Exception $e) { return new WP_REST_Response(['error' => $e->getMessage()], 403); } // Check if the provider. if ('email' !== $parameters->provider) { return new WP_REST_Response(array('error' => 'Invalid provider'), 401); } // Finally redirect the user to the redirect_to page with a response. return $this->start_email_validation($user->ID, $parameters->redirect_to, $parameters->profile); } /** * Sets the user provider as email and redirects the user to the specified page. * * @param WP_REST_Request $request The REST request object. * * @return WP_REST_Response The REST response object if user is not logged in or provider is invalid. */ public function set_as_email(WP_REST_Request $request): WP_REST_Response { $parameters = new Rsssl_Request_Parameters($request); // Verify the user and login nonce. try { $user = $this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce); } catch (Exception $e) { return new WP_REST_Response(['error' => $e->getMessage()], 403); } // Check if the provider. if ('email' !== $parameters->provider) { return new WP_REST_Response(array('error' => __('Invalid provider', 'really-simple-ssl')), 401); } // Finally redirect the user to the redirect_to page with a response. return $this->start_email_validation($user->ID, $parameters->redirect_to, $parameters->profile); } /** * Validates the email setup for a user. * * This function handles the validation of the email setup process. It checks the provided token * and updates the user's two-factor authentication status accordingly. If the token is invalid, * it resets the user's two-factor authentication settings and logs the user out. * * @param WP_REST_Request $request The REST request object containing the necessary parameters. * * @return WP_REST_Response The REST response object indicating the result of the validation process. */ public function validate_email_setup(WP_REST_Request $request): WP_REST_Response { // Extract parameters from the request. $parameters = new Rsssl_Request_Parameters($request); // Check if the provider is 'email'. if ('email' !== $parameters->provider) { return new WP_REST_Response(array('error' => 'Invalid provider'), 401); } try { $user = $this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce); } catch (Exception $e) { return new WP_REST_Response(['error' => $e->getMessage()], 403); } // Validate the provided token. if (!Rsssl_Two_Factor_Email::get_instance()->validate_token($user->ID, self::sanitize_token($parameters->token))) { // Reset all the settings if the token is invalid. Rsssl_Two_Factor_Email::set_user_status($user->ID, 'open'); // Log out the user. wp_logout(); return new WP_REST_Response(array('error' => __('Code was was invalid, try "Resend Code"', 'really-simple-ssl')), 401); } // Mark all other providers as inactive. self::set_active_provider($user->ID, 'email'); // Authenticate the user and redirect them to the specified URL. return $this->authenticate_and_redirect($user->ID, $parameters->redirect_to); } /** * Resends the email verification code for a user. * * @param WP_REST_Request $request The REST request object. * @return WP_REST_Response The REST response object. */ public function resend_email_code( WP_REST_Request $request ): WP_REST_Response { $parameters = new Rsssl_Request_Parameters($request); // Verify the user and login nonce. try { $user = $this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce); } catch (Exception $e) { return new WP_REST_Response(['error' => $e->getMessage()], 403); } // Sanitize and verify the provider. $provider = sanitize_text_field($parameters->provider); if ('email' !== $provider) { return new WP_REST_Response(['error' => __('Invalid provider', 'really-simple-ssl')], 400); } // Determine email 2FA status for this user. $email_status = get_user_meta($parameters->user_id, 'rsssl_two_fa_status_email', true ) ?? 'open'; $login_action = Rsssl_Two_Factor_Settings::get_login_action($parameters->user_id); // if the status has an empty value, set it to 'open' if (empty($email_status)) { $email_status = 'open'; } if ('active' !== $email_status && !('open' === $email_status && 'onboarding' === $login_action)) { return new WP_REST_Response([ 'error' => __('Email authentication is not active for this user', 'really-simple-ssl') ], 403); } // Generate and send a new token. try { Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, (bool) $parameters->profile); } catch (WP_Error $e) { return new WP_REST_Response(['error' => $e->get_error_message()], 500); } return new WP_REST_Response( ['message' => __('A verification code has been sent to the email address associated with your account.', 'really-simple-ssl')], 200 ); } ############################### # All support functions here # ############################### /** * Starts the process of email validation for a user. * * @param int $user_id The ID of the user for whom the email validation process needs to be started. * @param string $redirect_to The URL to redirect the user after the email validation process. Default is an empty string. * * @return WP_REST_Response The REST response object. */ private function start_email_validation(int $user_id, string $redirect_to = '', $profile = false): WP_REST_Response { $redirect_to = $redirect_to ?: home_url(); $user = get_user_by('id', $user_id); // Sending the email with the code. Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, $profile); $token = get_user_meta($user_id, Rsssl_Two_Factor_Email::RSSSL_TOKEN_META_KEY, true); if ($redirect_to === 'profile') { return new WP_REST_Response(array('token' => $token, 'validation_action' => 'validate_email_setup'), 200); } return new WP_REST_Response(array('token' => $token, 'redirect_to' => $redirect_to, 'validation_action' => 'validate_email_setup'), 200); } /** * Sanitizes a token. * * @param string $token The token to sanitize. * @param int $length The expected length of the token. Default is 0. * * @return string|false The sanitized token, or false if the length is invalid. */ public static function sanitize_token(string $token, int $length = 0) { $code = wp_unslash($token); $code = preg_replace('/\s+/', '', $code); // Maybe validate the length. if ($length && strlen($code) !== $length) { return false; } return (string)$code; } public function permission_callback_login_actions(WP_REST_Request $request) { $parameters = new Rsssl_Request_Parameters($request); $user_id = $parameters->user_id; $login_nonce = $parameters->login_nonce; // Ensure the login nonce is a string. if (!is_string($login_nonce)) { return new WP_Error( 'rest_forbidden', esc_html__('Access denied.', 'really-simple-ssl'), array('status' => 403) ); } // Use IP fetcher if available. if (class_exists('RSSSL\Pro\Security\WordPress\Limitlogin\Rsssl_IP_Fetcher')) { $ip_array_found = (new Rsssl_IP_Fetcher)->get_ip_address(); $ip_address = $ip_array_found[0] ?? $_SERVER['REMOTE_ADDR']; } else { // Fallback: use REMOTE_ADDR. $ip_address = $_SERVER['REMOTE_ADDR']; } // Validate the IP address. if (!filter_var($ip_address, FILTER_VALIDATE_IP)) { return new WP_Error( 'rest_forbidden', esc_html__('Access denied.', 'really-simple-ssl'), array('status' => 403) ); } // Rate limiting: build a transient key based on IP and route. $route = $request->get_route(); $transient_key = 'rsssl_rate_limit_' . md5($ip_address . $route); $attempts = get_transient($transient_key); if ($attempts === false) { $attempts = 0; } // Limit to 5 attempts. if ($attempts >= 5) { return new WP_Error( 'rest_forbidden', esc_html__('Too many attempts. Please try again later.', 'really-simple-ssl'), array('status' => 429) ); } // Verify the login nonce and that the user exists. if (!Rsssl_Two_Fa_Authentication::verify_login_nonce($user_id, $login_nonce) || !get_user_by('id', $user_id) ) { // Increment the attempt count. set_transient($transient_key, $attempts + 1, 10 * MINUTE_IN_SECONDS); return new WP_Error( 'rest_forbidden', esc_html__('Access denied.', 'really-simple-ssl'), array('status' => 403) ); } // Reset the rate-limit on successful validation. delete_transient($transient_key); return true; } } wordpress/two-fa/controllers/class-rsssl-base-controller.php 0000777 00000007202 15251751331 0020425 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Controllers; use Exception; use RSSSL\Pro\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Passkey; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Request_Parameters; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Provider_Loader; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Provider; use WP_REST_Request; use WP_REST_Response; final class Rsssl_Base_Controller extends Rsssl_Abstract_Controller { protected const METHOD = 'POST'; protected const FEATURE_ROUTE = '/two-fa'; protected string $namespace; public function __construct($namespace, $version, $featureVersion) { parent::__construct($namespace, $version, $featureVersion); add_action('rest_api_init', array($this, 'register_api_routes')); } /** * Registers the REST API routes for the base controller. * * @return void * @throws Exception */ public function register_api_routes(): void { $this->route($this->namespace, self::METHOD, 'do_not_ask_again', array($this, 'disable_two_fa_for_user'), null, $this->build_args(array('user_id', 'login_nonce'), array('redirect_to')) ); $this->route($this->namespace, self::METHOD, 'skip_onboarding', array($this, 'skip_onboarding'), null, $this->build_args(array('user_id', 'login_nonce'), array('redirect_to')) ); } /** * Disables two-factor authentication for the user. * * @param WP_REST_Request $request The REST request object. * * @return WP_REST_Response The REST response object. */ public function disable_two_fa_for_user( WP_REST_Request $request ): WP_REST_Response { $parameters = new Rsssl_Request_Parameters( $request ); try { $user = $this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce); } catch (Exception $e) { return new WP_REST_Response(['error' => $e->getMessage()], 403); } // if the 2FA is not enabled for the user, we only handle the passkey meta key if ( ! (bool) rsssl_get_option( 'login_protection_enabled' ) ) { // Remove the passkey meta key for the user. update_user_meta( $user->ID, 'rsssl_passkey_configured', 'ignored' ); // return $this->authenticate_and_redirect( $user->ID, $parameters->redirect_to ); } $loader = Rsssl_Provider_Loader::get_loader(); // We get all the available providers for the user. foreach ($loader::get_providers() as $provider ) { /** * Set the user status to disable. * * @var Rsssl_Two_Factor_Provider $provider */ $provider::set_user_status( $user->ID, 'disabled' ); } // Finally we redirect the user to the redirect_to page. return $this->authenticate_and_redirect( $user->ID, $parameters->redirect_to ); } /** * Skips the onboarding process for the user. * * @param WP_REST_Request $request The REST request object. * * @return WP_REST_Response The REST response object. */ public function skip_onboarding( WP_REST_Request $request ): WP_REST_Response { $parameters = new Rsssl_Request_Parameters( $request ); try { $user = $this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce); } catch (Exception $e) { return new WP_REST_Response(['error' => $e->getMessage()], 403); } return $this->authenticate_and_redirect( $user->ID, $parameters->redirect_to ); } } wordpress/two-fa/controllers/class-rsssl-two-fa-user-controller.php 0000777 00000003625 15251751331 0021671 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Controllers; use RSSSL\Security\WordPress\Two_Fa\Contracts\Rsssl_Two_Fa_User_Repository_Interface; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters; class Rsssl_Two_Fa_User_Controller { private Rsssl_Two_Fa_User_Repository_Interface $userRepository; /** * Rsssl_Two_Fa_User_Controller constructor. */ public function __construct( Rsssl_Two_Fa_User_Repository_Interface $userRepository ) { $this->userRepository = $userRepository; } /** * Get users for the admin overview. * * @return array */ public function getUsersForAdminOverview(Rsssl_Two_FA_Data_Parameters $params): array { $userCollection = $this->userRepository->getTwoFaUsers($params); $data = []; $negative_count = $params->negative_count; foreach ($userCollection->getUsers() as $twoFaUser) { if (empty($twoFaUser->getRoles())) { $negative_count++; continue; } // Directly use the domain object's getters. $data[] = [ 'ID' => $twoFaUser->getId(), 'user' => $twoFaUser->getUsername(), 'status_for_user' => $twoFaUser->getStatus(), 'rsssl_two_fa_providers' => $twoFaUser->getProvider(), 'user_role' => $twoFaUser->getRoles(), 'can_reset' => $twoFaUser->isStatusResettable(), ]; } return [ 'request_success' => true, 'data' => $data, 'totalRecords' => $userCollection->getTotalRecords() - $negative_count, 'offset' => $params->offset, 'number' => $params->number, 'negative_count' => $negative_count, ]; } } wordpress/two-fa/services/class-rsssl-callback-queue.php 0000777 00000004012 15251751331 0017461 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Services; class Rsssl_Callback_Queue { /** * Option name used to store the callback queue. * * @var string */ private $option_name = 'rsssl_callback_queue'; /** * Add a task to the queue. * * @param callable $callback The callback to run. * @param array $args The arguments to pass to the callback. * * @return void */ public function add_task(callable $callback, array $args = []): void { $queue = get_option($this->option_name, []); // Each task is an associative array with a callback and its args. $queue[] = [ 'callback' => $callback, 'args' => $args, 'timestamp' => time(), ]; $this->save_queue($queue); } /** * Retrieve the current queue. * * @return array */ public function get_queue(): array { return get_option($this->option_name, []); } /** * Save the modified queue. * * @param array $queue The updated queue. * * @return void */ public function save_queue(array $queue): void { update_option($this->option_name, $queue); } /** * Process a limited number of tasks from the queue. * * @param int $limit The maximum number of tasks to process. * * @return void */ public function process_tasks(int $limit = 1): void { $queue = $this->get_queue(); if (empty($queue)) { return; } // Process up to $limit tasks. $processed = 0; while (!empty($queue) && $processed < $limit) { $task = array_shift($queue); if (is_callable($task['callback'])) { // Call the callback with the stored arguments. call_user_func_array($task['callback'], $task['args']); } $processed++; } // Save the updated (remaining) queue. $this->save_queue($queue); } } wordpress/two-fa/services/class-rsssl-two-fa-forced-role-service.php 0000777 00000005227 15251751331 0021646 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Services; use RSSSL\Security\WordPress\Two_Fa\Contracts\Rsssl_Has_Processing_Interface; use RSSSL\Security\WordPress\Two_Fa\Contracts\Rsssl_Two_Fa_User_Repository_Interface; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_Fa_User_Collection; use RSSSL\Security\WordPress\Two_Fa\Repositories\Rsssl_Two_Fa_User_Repository; class Rsssl_Two_Fa_Forced_Role_Service implements Rsssl_Has_Processing_Interface { private Rsssl_Two_Fa_User_Repository_Interface $userRepository; private Rsssl_Two_FA_Data_Parameters $params; public function __construct(Rsssl_Two_FA_Data_Parameters $params) { $this->userRepository = new Rsssl_Two_Fa_User_Repository(); $this->params = $params; } /** * Process a batch of forced two-factor users with disabled status. * * @param string $statusType * @return Rsssl_Two_Fa_User_Collection */ public function processBatch(array $args, string $switchValue): Rsssl_Two_Fa_User_Collection { // We set a temp trancient to enable a debig check set_transient('rsssl_two_fa_forced_role_service', $args, 60); switch ($switchValue) { case 'disabled': return $this->userRepository->getForcedTwoFaUsersWithDisabledStatus($this->params, $args); case 'open': return $this->userRepository->getAddedForcedTwoFaUsersWithOpenStatus($this->params, $args); } } /** * Check if the forced roles have changed. * * @return array */ public static function getForForcedRolesChange(array $oldForcedRoles, array $newForcedRoles): array { // Check if the forced roles have changed. And only get the new Roles added. return array_diff($newForcedRoles, $oldForcedRoles); } /** * Reset the status of the forced users when the forced roles are added. */ public function maybeResetForcedUsersWhenDisabled(array $changedRoles): void { $collection = $this->userRepository->getForcedTwoFaUsersWithDisabledStatus($this->params, $changedRoles); foreach ($collection->getUsers() as $user) { $user->resetStatus(); } } /** * Reset the status of the forced users when the forced roles are added. */ public function maybeResetForcedUsersWhenExpired(array $changedRoles): void { $collection = $this->userRepository->getForcedTwoFaUsersWithExpiredStatus($this->params, $changedRoles); foreach ($collection->getUsers() as $user) { $user->resetStatus(); } } } wordpress/two-fa/services/class-rsssl-two-fa-reminder-service.php 0000777 00000013721 15251751331 0021250 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Services; use rsssl_mailer; use RSSSL\Security\WordPress\Two_Fa\Contracts\Rsssl_Two_Fa_User_Repository_Interface; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_Fa_User_Collection; use RSSSL\Security\WordPress\Two_Fa\Repositories\Rsssl_Two_Fa_User_Repository; class Rsssl_Two_Fa_Reminder_Service { private Rsssl_Two_Fa_User_Repository_Interface $userRepository; /** * Constructor. * * Here we hook our process method to a custom WP action. */ public function __construct() { $this->userRepository = new Rsssl_Two_Fa_User_Repository(); add_action('rsssl_process_two_fa_reminders', [$this, 'processReminders']); } /** * Kick off the reminder e‑mail process. * * Checks whether forced roles are set and then schedules a WP event * to process the reminder emails in a separate request. * * @return bool */ public function maybeSendReminderEmails(array $forcedRoles):bool { // If no forced roles have been defined, there is nothing to do. if (empty($forcedRoles)) { update_option('rsssl_two_fa_reminder_pending', false, false); return false; } // Checking if there are users within the grace period who have not yet configured 2FA. $params = new Rsssl_Two_FA_Data_Parameters([ 'filter_column' => 'user_role', 'filter_value' => 'all', ]); $users = $this->userRepository->getForcedTwoFaUsersWithOpenStatus($params); $this->processReminders($users); // no further processing is pending. So we return true. return true; } /** * Process and send reminder emails to users who have not yet configured 2FA. * * This method is hooked to a WP action and is executed via a scheduled event. * * @return void */ public function processReminders(Rsssl_Two_Fa_User_Collection $collection ): void { // if the collection has no users, there is nothing to do. if ($collection->getTotalRecords() === 0) { return; } // Preparing the reminder e‑mail. // Load the mailer class if it hasn't been loaded yet. if (!class_exists('rsssl_mailer')) { require_once rsssl_path . 'mailer/class-mail.php'; } // Build the e‑mail subject. $subject = __("Important security notice", "really-simple-ssl"); // Determine the login URL – use the custom login URL if one is set. $login_url = wp_login_url(); if (function_exists('rsssl_get_option') && rsssl_get_option('change_login_url_enabled') !== false && !empty(rsssl_get_option('change_login_url'))) { $login_url = trailingslashit(site_url()) . rsssl_get_option('change_login_url'); } // Build a login link. $login_link = sprintf( '<a href="%s">%s</a>', esc_url($login_url), __('Please login', 'really-simple-ssl') ); $message = sprintf( /* translators: 1: Site URL. */ __("You are receiving this email because you have an account registered at %s.", "really-simple-ssl"), site_url(), ); $message .= "<br><br>"; $message .= sprintf( /* translators: 1: Login link with the text "Please login". 2: Opening <strong> tag to emphasize the "within three days" text. 3: Closing </strong> tag for "within three days". 4: Opening <strong> tag to emphasize "you will be unable to login". 5 Closing </strong> tag for "you will be unable to login". */ __("The site's security policy requires you to configure Two-Factor Authentication to protect against account theft. %1\$s and configure Two-Factor authentication %2\$swithin three days%3\$s. If you haven't performed the configuration by then, %4\$syou will be unable to login%5\$s.", "really-simple-ssl"), $login_link, '<strong>', '</strong>', '<strong>', '</strong>' ); $mailer = new rsssl_mailer(); $mailer->subject = $subject; $mailer->branded = false; $mailer->sent_by_text = "<b>".sprintf( __( 'Notification by %s', 'really-simple-ssl' ), site_url() )."</b>"; $mailer->message = $message; // Process each user in the grace period who still needs to set up 2FA. foreach ($collection->getUsers() as $user) { // Skip if the reminder has already been sent. $two_fa_reminder_sent = get_user_meta($user->getId(), 'rsssl_two_fa_reminder_sent', true); if ($two_fa_reminder_sent) { continue; } $email = get_userdata($user->getId())->user_email; $first_name = get_userdata($user->getId())->first_name; $last_name = get_userdata($user->getId())->last_name; // Prepare and send the e‑mail. $mailer->template_filename = apply_filters('rsssl_email_template', rsssl_path . '/mailer/templates/email-unbranded.html'); $mailer->to = $email; $mailer->title = sprintf( /* translators: %s: First name. %s: Last name. */ __('Hi %s %s', 'really-simple-ssl'), trim($first_name), trim($last_name) ) . ','; $mailer->message = $message; $mailer->send_mail(); // Mark that the reminder e‑mail has been sent for this user. update_user_meta($user->getId(), 'rsssl_two_fa_reminder_sent', true); } // Optionally, update a flag to indicate that no further processing is pending. update_option('rsssl_two_fa_reminder_pending', false, false); } } wordpress/two-fa/services/class-rsssl-two-fa-status-service.php 0000777 00000003600 15251751331 0020761 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Services; class Rsssl_Two_Fa_Status_Service { /** * Determine the two-factor status for a user. * * @return string */ public function determineStatus(int $userId, array $forcedRoles, int $daysThreshold): string { $totpStatus = get_user_meta($userId, 'rsssl_two_fa_status_totp', true); $emailStatus = get_user_meta($userId, 'rsssl_two_fa_status_email', true); $passkeyStatus = get_user_meta($userId, 'rsssl_two_fa_status_passkey', true); $lastLogin = get_user_meta($userId, 'rsssl_two_fa_last_login', true); // User has active 2FA configured if (in_array('active', [$totpStatus, $emailStatus, $passkeyStatus], true)) { return 'active'; } // User has explicitly disabled 2FA if ($totpStatus === 'disabled' && $emailStatus === 'disabled') { return 'disabled'; } // Check if user has a forced role $userData = get_userdata($userId); $userRoles = $userData ? $userData->roles : []; $isForced = !empty(array_intersect($forcedRoles, $userRoles)); // Non-forced user: return based on method status or default to open if (!$isForced) { return $totpStatus ?: $emailStatus ?: 'open'; } // New user without lastLogin - initialize grace period if (empty($lastLogin)) { update_user_meta($userId, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s')); return 'open'; } // Grace period has expired $lastLoginTime = strtotime($lastLogin); $thresholdTime = strtotime("-$daysThreshold days"); if ($lastLoginTime !== false && $lastLoginTime < $thresholdTime) { return 'expired'; } // Still within grace period return $totpStatus ?: $emailStatus ?: 'open'; } } wordpress/two-fa/services/class-rsssl-two-factor-reset-service.php 0000777 00000006155 15251751331 0021460 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Services; use RSSSL\Security\WordPress\Two_Fa\Contracts\Rsssl_Two_Fa_User_Repository_Interface; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters; use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Status; // Assuming this provides delete_two_fa_meta() class Rsssl_Two_Factor_Reset_Service { private Rsssl_Two_Fa_User_Repository_Interface $userRepository; /** * Inject the repository and hook the batched process callback. */ public function __construct(Rsssl_Two_Fa_User_Repository_Interface $userRepository) { $this->userRepository = $userRepository; // Hook the WP action for processing batches add_action('rsssl_process_batched_users', [$this, 'batchedProcess'], 10, 3); } /** * Kick off the reset fix process. * * This method creates a parameters object that signals that we only want expired users, * then queries the repository for the count of expired users. If any are found, it schedules * a WP event to process them in batches. * * @return void */ public function resetFix(): void { //Building base params $params = new Rsssl_Two_FA_Data_Parameters([ 'filter_column' => 'user_role', 'filter_value' => 'all', ]); // no need to run if there are no forced roles if (empty($params->getForcedRoles())) { update_option('rsssl_reset_fix', false, false); return; } $params->setNumber(1000); //Setting the batch size $expired_users = $this->userRepository->geTwoFAExpiredUsers($params); if ($expired_users->getTotalRecords() > 0) { wp_schedule_single_event(time() + 20, 'rsssl_process_batched_users', [$expired_users->getUsers(), $expired_users->getTotalRecords(), $params->number]); } else { update_option('rsssl_reset_fix', false, false); } } /** * Process expired users in batches. * * This method is called via the scheduled WP event. It uses the repository to fetch * a batch of expired users (based on the passed-in parameters) and resets the two-factor * status on each user. * * @return void */ public function batchedProcess(Rsssl_Two_FA_Data_Parameters $params, int $user_count, int $batch_size = 500): void { // Loop until all expired users have been processed. while ($user_count > 0) { $params->number = $batch_size; // Fetch a batch of users via the repository. $usersCollection = $this->userRepository->getTwoFaUsers($params); foreach ($usersCollection->getUsers() as $twoFaUser) { // Delete the two-factor meta for the user. Rsssl_Two_Fa_Status::delete_two_fa_meta($twoFaUser->getId()); // Update the last login meta so that the user is forced to re‑authenticate with 2FA. update_user_meta($twoFaUser->getId(), 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s')); } $user_count -= $batch_size; } } } wordpress/two-fa/class-rsssl-parameter-validation.php 0000777 00000012073 15251751331 0017076 0 ustar 00 <?php /** * Holds the request parameters for a specific action. * This class holds the request parameters for a specific action. * It is used to store the parameters and pass them to the functions. * * @package REALLY_SIMPLE_SSL */ namespace RSSSL\Security\WordPress\Two_Fa; use WP_User; /** * Holds the request parameters for a specific action. * This class holds the request parameters for a specific action. * It is used to store the parameters and pass them to the functions. * * @package REALLY_SIMPLE_SSL */ class Rsssl_Parameter_Validation { /** * Validates a user ID. * * @param int $user_id The user ID to be validated. * * @return void */ public static function validate_user_id(int $user_id): void { if (!is_numeric($user_id)) { // Create an error message for the profile page. add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-error', __('The user ID is not valid.', 'really-simple-ssl') ); } } /** * Validates post data. * * @param array $post_data The post data to validate. * * @return void */ public static function validate_post_data(array $post_data): void { if (!isset($post_data['preferred_method'])) { // Create an error message for the profile page. add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-error', __('The preferred method is not set.', 'really-simple-ssl') ); } } /** * Validate user object. * * @param mixed $user The user object to validate. * * @return void */ public static function validate_user($user): void { if (!$user instanceof WP_User) { // Create an error message for the profile page. add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-error', __('The user object is not valid.', 'really-simple-ssl') ); } } /** * Validates the selected provider. * * @param string $selected_provider The selected provider to validate. * * @return void */ public static function validate_selected_provider(string $selected_provider): void { if (!in_array($selected_provider, array('totp', 'email', 'none'), true)) { // Create an error message for the profile page. add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-error', __('The selected provider is not valid.', 'really-simple-ssl') ); } } /** * Validates an authentication code. * * @param mixed $auth_code The authentication code to validate. * * @return void */ public static function validate_auth_code($auth_code): void { if (!is_numeric($auth_code)) { // Create an error message for the profile page. add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-error', __('The authentication code is not valid.', 'really-simple-ssl') ); } } /** * Validates a given key. * * @param mixed $key The key to validate. * * @return void */ public static function validate_key($key): void { if (!is_string($key)) { // Create an error message for the profile page. add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-error', __('The key is not valid.', 'really-simple-ssl') ); } } /** * Cache the current errors for a user in a transient. * * @param int $user_id The ID of the user. * * @return void */ public static function cache_errors(int $user_id): void { // Put the current errors in a transient. set_transient('rsssl_two_factor_auth_error_' . $user_id, get_settings_errors(), 60); } /** * Retrieves cached errors for a specific user. * * @param int $user_id The ID of the user to retrieve the errors for. * * @return mixed|null An array of errors if found, null otherwise. */ public static function get_cached_errors(int $user_id) { // Get the errors from the transient. $errors = get_transient('rsssl_two_factor_auth_error_' . $user_id); // Delete the transient. delete_transient('rsssl_two_factor_auth_error_' . $user_id); return $errors; } /** * Deletes cached errors for a specific user. * * @param int $user_id The ID of the user to delete the errors for. * * @return void */ public static function delete_cached_errors(int $user_id): void { delete_transient('rsssl_two_factor_auth_error_' . $user_id); } } wordpress/two-fa/class-rsssl-two-fa-data-parameters.php 0000777 00000005707 15251751331 0017241 0 ustar 00 <?php /** * Two-Factor Authentication Data Parameters helper. * * @package REALLY_SIMPLE_SSL * @since 0.1-dev */ namespace RSSSL\Security\WordPress\Two_Fa; /** * Class Rsssl_Two_FA_Data_Parameters * * Represents the data parameters for the Two FA data. * * @package REALLY_SIMPLE_SSL */ class Rsssl_Two_FA_Data_Parameters { /** * The current page name. * * @var string $page The current page name. */ public string $page; /** * The number of items to display per page. * * @var int $page_size The number of items to display per page. */ public int $page_size; /** * The search term entered by the user. * * @var string $search_term The search term entered by the user */ public string $search_term; /** * The value used for filtering. * * @var string|null $filter_value This variable stores the value used for filtering. */ public string $filter_value; /** * The column used for filtering. * * @var string|null $filter_column This variable stores the column used for filtering. */ public string $filter_column; /** * The column used for sorting. * * @var string|null $sort_column This variable stores the column used for sorting. */ public string $sort_column; /** * The direction of the sorting, can be 'asc' or 'desc'. * * @var string $sort_direction The direction of the sorting, can be 'asc' or 'desc' */ public string $sort_direction; /** * The HTTP method used for the current request, can be 'GET', 'POST', 'PUT', 'DELETE', etc. * * @var string $method The HTTP method used for the current request, can be 'GET', 'POST', 'PUT', 'DELETE', etc. */ public string $method; /** * The allowed filters. * * @var array $allowed_filters The allowed filters. */ private const allowed_filters = array( 'all', 'open', 'disabled', 'active', 'expired' ); /** * Constructs a new object with given data. * * @param array $data The data array. */ public function __construct( array $data ) { $this->page = isset( $data['currentPage'] ) ? (int) $data['currentPage'] : 1; $this->page_size = isset( $data['currentRowsPerPage'] ) ? (int) $data['currentRowsPerPage'] : 5; $this->search_term = isset( $data['search'] ) ? sanitize_text_field( $data['search'] ) : ''; $this->filter_value = in_array( $data['filterValue'] ?? 'all', self::allowed_filters, true ) ? sanitize_text_field( $data['filterValue'] ?? 'all') : 'all'; $this->sort_direction = in_array( strtoupper( $data['sortDirection'] ?? 'DESC' ), array( 'ASC', 'DESC' ), true ) ? strtoupper( sanitize_text_field( $data['sortDirection'] ?? 'DESC')) : 'DESC'; $this->filter_column = isset( $data['filterColumn'] ) ? sanitize_text_field( $data['filterColumn'] ) : 'rsssl_two_fa_status'; $this->sort_column = isset( $data['sortColumn'] ) ? sanitize_text_field( $data['sortColumn'] ) : 'user'; $this->method = isset( $data['method'] ) ? Rsssl_Two_Factor_Settings::sanitize_method( $data['method'] ) : 'email'; } } wordpress/two-fa/traits/trait-rsssl-email-trait.php 0000777 00000012724 15251751331 0016525 0 ustar 00 <?php /** * Trait for sending emails related to two-factor authentication. * * @package RSSSL\Pro\Security\WordPress\Two_Fa\Traits */ namespace RSSSL\Security\WordPress\Two_Fa\Traits; use rsssl_mailer; use WP_User; /** * Trait Rsssl_Email_Trait * * This trait handles email notifications related to password reset and compromised passwords. */ trait Rsssl_Email_Trait { /** * Notify the user that their password has been compromised and reset. * * @param WP_User $user The user to notify. * * @return void */ public static function notify_user_password_reset( WP_User $user ): void { $subject = __( 'Your password was compromised and has been reset', 'really-simple-ssl' ); $message = self::create_user_message( $user ); if ( ! class_exists( 'rsssl_mailer' ) ) { require_once rsssl_path . 'mailer/class-mail.php'; } $mailer = self::initialize_mailer( $subject, $message, $user ); $mailer->send_mail(); } /** * Create a user message for failed login attempts. * * @param WP_User $user The user object. * * @return string The user message. */ private static function create_user_message( WP_User $user ): string { $message = sprintf( /* translators: %1$s: user login, %2$s: site url, %3$s: password best practices link, %4$s: lost password url */ __( 'Hello %1$s, an unusually high number of failed login attempts have been detected on your account at %2$s. These attempts successfully entered your password, and were only blocked because they failed to enter your second authentication factor. Despite not being able to access your account, this behavior indicates that the attackers have compromised your password. The most common reasons for this are that your password was easy to guess, or was reused on another site which has been compromised. To protect your account, your password has been reset, and you will need to create a new one. For advice on setting a strong password, please read %3$s To pick a new password, please visit %4$s This is an automated notification. If you would like to speak to a site administrator, please contact them directly.', 'really-simple-ssl' ), esc_html( $user->user_login ), home_url(), 'https://wordpress.org/documentation/article/password-best-practices/', esc_url( add_query_arg( 'action', 'lostpassword', rsssl_wp_login_url() ) ) ); return str_replace( "\t", '', $message ); } /** * Notify the admin that a user's password was compromised and reset. * * @param WP_User $user The user whose password was reset. * * @return void */ public static function notify_admin_user_password_reset( WP_User $user ): void { if ( ! class_exists( 'rsssl_mailer' ) ) { require_once rsssl_path . 'mailer/class-mail.php'; } $subject = self::create_subject( $user ); $message = self::create_message( $user ); $mailer = self::initialize_mailer( $subject, $message, $user ); $mailer->send_mail(); } /** * Create subject for the compromised password reset email. * * @param WP_User $user The user object. * * @return string The subject of the email. */ private static function create_subject( WP_User $user ): string { /* translators: %s: user login */ return sprintf( __( 'Compromised password for %s has been reset', 'really-simple-ssl' ), esc_html( $user->user_login ) ); } /** * Generate a message for notifying the user about a high number of failed login attempts. * * @param WP_User $user The user for whom the message is created. * * @return string The generated message. */ private static function create_message( WP_User $user ): string { $documentation_url = 'https://developer.wordpress.org/plugins/hooks/'; return str_replace( "\t", '', // translators: %1$s: user login, %2$d: user ID, %3$s: documentation URL. sprintf( __( 'Hello, this is a notice from your website to inform you that an unusually high number of failed login attempts have been detected on the %1$s account (ID %2$d). Those attempts successfully entered the user\'s password, and were only blocked because they entered invalid second authentication factors. To protect their account, the password has automatically been reset, and they have been notified that they will need to create a new one. If you do not wish to receive these notifications, you can disable them with the `two_factor_notify_admin_user_password_reset` filter. See %3$s for more information. Thank you', 'really-simple-ssl' ), esc_html( $user->user_login ), $user->ID, $documentation_url ) ); } /** * Initialize the mailer for sending a notification email. * * @param string $subject The subject of the email. * @param string $message The message content of the email. * @param WP_User $user The user object to send the email to. * * @return rsssl_mailer The initialized mailer object. */ private static function initialize_mailer( string $subject, string $message, WP_User $user ): rsssl_mailer { $mailer = new rsssl_mailer(); $mailer->subject = $subject; $mailer->branded = false; $mailer->sent_by_text = "<b>" . sprintf( __( 'Notification by %s', 'really-simple-ssl' ), site_url() ) . "</b>"; $mailer->template_filename = apply_filters( 'rsssl_email_template', rsssl_path . '/mailer/templates/email-unbranded.html' ); $mailer->to = $user->user_email; $mailer->title = __( 'Compromised password reset', 'really-simple-ssl' ); $mailer->message = $message; return $mailer; } } wordpress/two-fa/traits/trait-rsssl-two-fa-helper.php 0000777 00000012142 15251751331 0016761 0 ustar 00 <?php /** * A helper trait for sanitizing status and method values. * * @package really-simple-ssl */ namespace RSSSL\Security\WordPress\Two_Fa\Traits; use FG\ASN1\Universal\Boolean; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Provider_Loader; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Provider; use RSSSL\Security\WordPress\Two_Fa\Rsssl_Request_Parameters; use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Authentication; use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Status; use WP_REST_Request; use WP_REST_Response; /** * A helper trait for sanitizing status and method values. */ trait Rsssl_Two_Fa_Helper { /** * Sanitize the given status. * * @param string $status The status to sanitize. * * @return string The sanitized status. */ private static function sanitize_status( string $status ): string { $statuses_available = Rsssl_Two_Fa_Status::STATUSES; if ( empty( $status ) ) { return 'open'; } // Check if the $status is in the array of available statuses. if ( ! in_array( $status, $statuses_available, true ) ) { // if not, set it to 'disabled'. $status = 'disabled'; } return sanitize_text_field( $status ); } /** * Sanitize a given method. * * @param string $method The method to sanitize. * * @return string The sanitized method. */ private static function sanitize_method( string $two_fa_provider ): string { $loader = Rsssl_Provider_Loader::get_loader(); $two_fa_providers_available = $loader::TWO_FA_PROVIDERS; // Check if the $method is in the array of available methods. if ( ! in_array( $two_fa_provider, $two_fa_providers_available, true ) ) { // if not, set it to 'disabled'. $two_fa_provider = 'disabled'; } return sanitize_text_field( $two_fa_provider ); } /** * Checks if the requested namespace matches our specific namespace and bypasses authentication. * * @param WP_REST_Request $request The REST request object. */ private function check_custom_validation( WP_REST_Request $request ): bool { // first check if the $-REQUEST['rest_route'] is set. $params = new Rsssl_Request_Parameters( $request ); if ( ! isset( $params->login_nonce ) ) { return false; } return Rsssl_Two_Fa_Authentication::verify_login_nonce( $params->user_id, $params->login_nonce ); } /** * Verifies a login nonce, gets user by the user id, and returns an error response if any steps fail. * * @param int $user_id The user ID. * @param string $login_nonce The login nonce. * * @return bool */ private function verify_hashed_user_id( int $user_id, string $login_nonce ): bool { return Rsssl_Two_Fa_Authentication::verify_login_nonce( $user_id, $login_nonce ); } /** * Sets the authentication cookie and returns a success response. * * @param int $user_id The user ID. * @param string $redirect_to The redirect URL. * * @return WP_REST_Response */ public function authenticate_and_redirect( int $user_id, string $redirect_to = '' ): WP_REST_Response { // Okay checked the provider now authenticate the user. wp_set_auth_cookie( $user_id, true ); // Finally redirect the user to the redirect_to page or to the home page if the redirect_to is not set. $redirect_to = $redirect_to ?: home_url(); return new WP_REST_Response( array( 'redirect_to' => $redirect_to ), 200 ); } /** * Sets the active provider for a user. * * This function loops through all available providers and sets the status of each provider. * The provider that matches the allowed method is set to 'active', while all other providers are set to 'disabled'. * * @param int $user_id The ID of the user. * @param string $allowed_method The method that is allowed and should be set to 'active'. * @return void */ public static function set_active_provider(int $user_id, string $allowed_method): void { $user = get_userdata($user_id); $providers = Rsssl_Provider_Loader::get_loader()::get_enabled_providers_for_user($user); foreach ($providers as $provider) { /** @var Rsssl_Two_Factor_Provider $provider */ if ($provider::METHOD !== $allowed_method) { $provider::reset_meta_data($user_id); $provider::set_user_status($user_id, 'disabled'); } else { $provider::set_user_status($user_id, 'active'); } } } /** * Sanitizes a token. * * @param string $token The token to sanitize. * @param int $length The expected length of the token. Default is 0. * * @return string|false The sanitized token, or false if the length is invalid. */ public static function sanitize_token(string $token, int $length = 0 ) { $code = wp_unslash( $token ); $code = preg_replace( '/\s+/', '', $code ); // Maybe validate the length. if ( $length && strlen( $code ) !== $length ) { return false; } return (string) $code; } } wordpress/two-fa/traits/trait-rsssl-args-builder.php 0000777 00000002773 15251751331 0016700 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Traits; trait Rsssl_Args_Builder { /** * Builds the arguments array for REST API routes. * * @param array $properties The properties to include in the arguments array. * @param array $optional_properties The properties to include as optional in the arguments array. * @return array The built arguments array. */ public function build_args(array $properties, array $optional_properties = array()): array { $args = array(); foreach ($properties as $property) { $args[$property] = array( 'required' => true, 'type' => $this->get_property_type($property), ); } foreach ($optional_properties as $property) { $args[$property] = array( 'required' => false, 'type' => $this->get_property_type($property), ); } return $args; } /** * Determines the type of a property. * * @param string $property The property name. * @return string The type of the property. */ private function get_property_type(string $property): string { $types = array( 'provider' => 'string', 'user_id' => 'integer', 'login_nonce' => 'string', 'redirect_to' => 'string', 'two-factor-totp-authcode' => 'string', 'key' => 'string', ); return $types[$property] ?? 'string'; } } wordpress/two-fa/class-rsssl-two-factor.php 0000777 00000157502 15251751331 0015062 0 ustar 00 <?php /** * This package is based on the WordPress feature plugin https://wordpress.org/plugins/two-factor/ * * Class for creating two-factor authorization. * * @since 7.0.6 * @noinspection OffsetOperationsInspection * @noinspection UnknownInspectionInspection * @package RSSSL\Pro\Security\WordPress\Two_Fa */ namespace RSSSL\Security\WordPress\Two_Fa; use Exception; use RSSSL\Security\WordPress\Two_Fa\Repositories\Rsssl_Two_Fa_User_Repository; use RSSSL\Security\WordPress\Two_Fa\Services\Rsssl_Two_Fa_Reminder_Service; use RSSSL\Security\WordPress\Two_Fa\Services\Rsssl_Two_Factor_Reset_Service; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Provider_Loader; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Provider; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Provider_Interface; use RSSSL\Security\WordPress\Two_Fa\Traits\Rsssl_Email_Trait; use WP_Error; use WP_Session_Tokens; use WP_User; /** * Class Rsssl_Two_Factor. * * The Rsssl_Two_Factor class provides methods for managing two-factor authentication for users. * * @package Rsssl */ class Rsssl_Two_Factor { use Rsssl_Email_Trait; /** * The user meta key to store the last failed timestamp. * * @type string */ public const RSSSL_USER_RATE_LIMIT_KEY = '_rsssl_two_factor_last_login_failure'; /** * The user meta key to store the number of failed login attempts. * * @var string */ public const RSSSL_USER_FAILED_LOGIN_ATTEMPTS_KEY = '_rsssl_two_factor_failed_login_attempts'; /** * The user meta key to store whether the password was reset. * * @var string */ public const RSSSL_USER_PASSWORD_WAS_RESET_KEY = '_rsssl_two_factor_password_was_reset'; /** * URL query parameter used for our custom actions. * * @var string */ public const RSSSL_USER_SETTINGS_ACTION_QUERY_VAR = 'rsssl_two_factor_action'; /** * Nonce key for user settings. * * @var string */ public const RSSSL_USER_SETTINGS_ACTION_NONCE_QUERY_ARG = '_rsssl_two_factor_action_nonce'; public const RSSSL_USER_META_ONBOARDING_COMPLETE = 'rsssl_two_fa_onboarding_complete'; /** * Namespace for plugin rest api endpoints. * * @var string */ public const REST_NAMESPACE = 'really-simple-security/v1/two-fa/v2'; /** * Keep track of all the password-based authentication sessions that * need to invalidated before the second factor authentication. * * @var array */ private static array $password_auth_tokens = array(); /** * Set up filters and actions. * * @param object $compat A compatibility layer for plugins. * * @since 0.1-dev */ public static function add_hooks(object $compat): void { if ( ( defined( 'RSSSL_DISABLE_2FA' ) && RSSSL_DISABLE_2FA ) || ( defined( 'RSSSL_SAFE_MODE' ) && RSSSL_SAFE_MODE ) ) { if ( rsssl_admin_logged_in() ) { ( new Rsssl_Two_Factor_Admin() ); } ( new Rsssl_Two_Factor_On_Board_Api() ); if ( is_user_logged_in() ) { (Rsssl_Two_Factor_Profile_Settings::get_instance()); } return; } /** * Runs the fix for the reset error in 9.1.1 */ if (filter_var(get_option('rsssl_reset_fix', false), FILTER_VALIDATE_BOOLEAN)) { $repository = new Rsssl_Two_Fa_User_Repository(); (new Rsssl_Two_Factor_Reset_Service($repository))->resetFix(); } // add_action( 'login_enqueue_scripts', array( __CLASS__, 'twofa_scripts' ) ); add_action('init', array(Rsssl_Provider_Loader::class, 'get_providers')); add_action('wp_login', array(__CLASS__, 'rsssl_wp_login'), 10, 2); add_action('wp_login_errors', array(__CLASS__, 'show_expired_onboarding_error')); add_filter('wp_login_errors', array(__CLASS__, 'rsssl_maybe_show_reset_password_notice')); add_action('after_password_reset', array(__CLASS__, 'rsssl_clear_password_reset_notice')); add_action('login_form_validate_2fa', array(__CLASS__, 'rsssl_login_form_validate_2fa')); // Loading the styles. add_action('login_enqueue_scripts', array(__CLASS__, 'enqueue_onboarding_styles')); if (rsssl_admin_logged_in()) { (new Rsssl_Two_Factor_Admin()); } ( new Rsssl_Two_Factor_On_Board_Api() ); if(is_user_logged_in()) { Rsssl_Two_Factor_Profile_Settings::get_instance(); } //add_action('rsssl_upgrade', array(__CLASS__, 'upgrade')); self::upgrade(); // Add the localized script for WP_REST. /** * Keep track of all the user sessions for which we need to invalidate the * authentication cookies set during the initial password check. * * Is there a better way of doing this? */ add_action('set_auth_cookie', array(__CLASS__, 'rsssl_collect_auth_cookie_tokens')); add_action('set_logged_in_cookie', array(__CLASS__, 'rsssl_collect_auth_cookie_tokens')); if ( isset( $_GET['rsssl_one_time_login'], $_GET['_wpnonce'] ) ) { $nonce = sanitize_text_field(wp_unslash($_GET['_wpnonce'])); if (wp_verify_nonce($nonce)) { add_action('init', array(__CLASS__, 'maybe_skip_auth')); } self::maybe_skip_auth(); } add_action('init', array(__CLASS__, 'rsssl_collect_auth_cookie_tokens')); // Run only after the core wp_authenticate_username_password() check. add_filter('authenticate', array(__CLASS__, 'rsssl_filter_authenticate')); // Run as late as possible to prevent other plugins from unintentionally bypassing. add_filter('authenticate', array(__CLASS__, 'rsssl_filter_authenticate_block_cookies'), PHP_INT_MAX); add_action('admin_init', array(__CLASS__, 'rsssl_enable_dummy_method_for_debug')); add_filter('rsssl_two_factor_providers', array(__CLASS__, 'enable_dummy_method_for_debug')); add_action( 'rsssl_daily_cron', array( __CLASS__, 'maybe_send_reminder_email' ) ); add_action( 'user_register', [__CLASS__, 'set_2fa_activation_date'], 10, 1 ); $compat->init(); } /** * @return void * * Send a reminder e-mail if Two FA has not been configured within 3 days. */ public static function maybe_send_reminder_email():void { $forcedRoles = rsssl_get_option('two_fa_forced_roles', []); if(empty($forcedRoles)) { return; } (new Rsssl_Two_Fa_Reminder_Service())->maybeSendReminderEmails($forcedRoles); } /** * Simple Date setter for Two Factor Forced roles. * @param $user_id * @return void */ public static function set_2fa_activation_date($user_id): void { // Get the user data; if not found, return early. $user_data = get_userdata($user_id); if (!$user_data) { return; } $user_roles = $user_data->roles; // Ensure forced roles is an array (empty if not set). $forcedRoles = rsssl_get_option('two_fa_forced_roles') ?: []; // If there is no intersection between forced roles and user's roles, do nothing. if (!array_intersect($forcedRoles, $user_roles)) { return; } // TODO: I really regret the meta_key name here. It should be rsssl_two_fa_activation_date. Need to fix this in the future. update_user_meta($user_id, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s')); } /** * Upgrade the two-factor login configuration. * * This method updates the configuration of two-factor login if necessary. * It checks if the login protection is enabled, if the plugin has been upgraded, * and if the enabled roles for email and TOTP need to be updated. * * @return void */ public static function upgrade(): void { if (rsssl_get_option('login_protection_enabled') && get_option('rsssl_two_fa_upgrade', false) === false) { // The way roles configuration was has now been changed. This means the forced roles and enabled roles need to change. $forced_roles = rsssl_get_option('two_fa_forced_roles'); $optional_roles = rsssl_get_option('two_fa_optional_roles'); $forced_roles = ($forced_roles !== false) ? $forced_roles : []; $optional_roles = ($optional_roles !== false) ? $optional_roles : []; // Merge the forced and optional roles into one array with unique values. $enabled_roles = array_unique(array_merge($forced_roles, $optional_roles)); if (empty($optional_roles)) { // no roles were set so ending the upgrade. return; } if (function_exists('rsssl_update_option')) { // Update the enabled roles for only email. rsssl_update_option('two_fa_enabled_roles_email', $enabled_roles); rsssl_update_option('two_fa_enabled_roles_totp', ['administrator']); // update the forced roles. rsssl_update_option('two_fa_forced_roles', $forced_roles); } // fetching the users that have active 2FA enabled. $users = get_users(array('meta_key' => 'rsssl_two_fa_status_email', 'meta_value' => 'active')); foreach ($users as $user) { Rsssl_Two_Fa_Status::set_active_provider($user->ID, 'email'); } update_option('rsssl_two_fa_upgrade', rsssl_version, false); } } /** * Enqueue the two-factor authentication scripts. * * @return void * * Allow 2FA bypass if status is open. */ public static function maybe_skip_auth(): void { if (isset($_GET['rsssl_one_time_login'], $_GET['token'], $_GET['_wpnonce'])) { // Unslash and sanitize. $rsssl_one_time_login = sanitize_text_field(wp_unslash($_GET['rsssl_one_time_login'])); $user_id = (int)Rsssl_Two_Factor_Settings::deobfuscate_user_id($rsssl_one_time_login); $user = get_user_by('id', $user_id); // Verify the nonce. $nonce = sanitize_text_field(wp_unslash($_GET['_wpnonce'])); if (!wp_verify_nonce($nonce, 'one_time_login_' . $user_id)) { wp_safe_redirect(wp_login_url() . '?login_error=nonce_invalid'); exit; } // Retrieve the stored token from the transient. $stored_token = get_transient('skip_two_fa_token_' . $user_id); // Check if the token is valid and not expired. $token = sanitize_text_field(wp_unslash($_GET['token'])); if ($user && $stored_token && hash_equals($stored_token, $token)) { // Delete the transient to invalidate the token. delete_transient('skip_two_fa_token_' . $user_id); $status = get_user_meta($user->ID, 'rsssl_two_fa_status_email', true); // Only allow skipping for users which have 2FA value open. if (isset($_GET['rsssl_two_fa_disable']) && 'open' === $status) { update_user_meta($user_id, 'rsssl_two_fa_status_email', 'disabled'); } if ('open' === Rsssl_Two_Factor_Settings::get_user_status('email', $user_id)) { update_user_meta($user_id, 'rsssl_two_fa_status_email', 'active'); update_user_meta($user_id, 'rsssl_two_fa_status_totp', 'disabled'); } delete_user_meta( $user_id, '_rsssl_factor_email_token' ); delete_user_meta( $user_id, '_rsssl_two_factor_backup_codes' ); wp_set_auth_cookie($user_id); wp_safe_redirect(admin_url()); exit; } // The token is invalid or expired. // Redirect to the login page with an error message or handle it as needed. wp_safe_redirect(wp_login_url() . '?login_error=token_invalid'); exit; } } /** * Enable the dummy method only during debugging. * * @param array $methods List of enabled methods. * * @return array */ public static function enable_dummy_method_for_debug(array $methods): array { if (!self::is_wp_debug()) { unset($methods['Two_Factor_Dummy']); } return $methods; } /** * Check if the debug mode is enabled. * * @return boolean */ protected static function is_wp_debug(): bool { return (defined('WP_DEBUG') && WP_DEBUG); } /** * Check if a user action is valid. * * @param integer $user_id User ID. * @param string $action User action ID. * * @return boolean */ public static function is_valid_user_action(int $user_id, string $action): bool { $request_nonce = isset($_REQUEST[self::RSSSL_USER_SETTINGS_ACTION_NONCE_QUERY_ARG]) ? sanitize_text_field(wp_unslash($_REQUEST[self::RSSSL_USER_SETTINGS_ACTION_NONCE_QUERY_ARG])) : ''; if (!$user_id || !$action || !$request_nonce) { return false; } return wp_verify_nonce( $request_nonce, sprintf('%d-%s', $user_id, $action) ); } /** * Get the ID of the user being edited. * * @return integer */ public static function current_user_being_edited(): int { // Try to resolve the user ID from the request first. if (!empty($_REQUEST['rsssl_user_id']) && !empty($_REQUEST['rsssl-action-nonce'])) { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['rsssl-action-nonce'])), 'rsssl-user-action')) { wp_die('Invalid nonce'); } $user_id = (int)$_REQUEST['rsssl_user_id']; if (current_user_can('edit_user', $user_id)) { return $user_id; } } return get_current_user_id(); } /** * Trigger our custom update action if a valid * action request is detected and passes the nonce check. * * @return void */ public static function rsssl_enable_dummy_method_for_debug(): void { $nonce = isset($_POST['nonce_field']) ? sanitize_text_field(wp_unslash($_POST['nonce_field'])) : ''; // Verify the nonce. if (!wp_verify_nonce($nonce, 'rsssl_user_action')) { return; } $action = isset($_REQUEST[self::RSSSL_USER_SETTINGS_ACTION_QUERY_VAR]) ? sanitize_text_field(wp_unslash($_REQUEST[self::RSSSL_USER_SETTINGS_ACTION_QUERY_VAR])) : ''; $user_id = self::current_user_being_edited(); if (self::is_valid_user_action($user_id, $action)) { /** * This action is triggered when a valid Two Factor settings * action is detected, and it passes the nonce validation. * * @param integer $user_id User ID. * @param string $action Settings action. */ do_action('rsssl_two_factor_user_settings_action', $user_id, $action); } } /** * Keep track of all the authentication cookies that need to be * invalidated before the second factor authentication. * * @param string $cookie Cookie string. * * @return void */ public static function rsssl_collect_auth_cookie_tokens(string $cookie): void { $parsed = wp_parse_auth_cookie($cookie); if (!empty($parsed['token'])) { self::$password_auth_tokens[] = $parsed['token']; } } /** * Get all Two-Factor Auth providers that are both enabled and configured for the specified|current user. * * @param WP_User $user Optional. User ID, or WP_User object of the user. Defaults to current user. * * @return array */ public static function get_available_providers_for_user(WP_User $user): array { $loader = Rsssl_Provider_Loader::get_loader(); return $loader::available_providers(); } /** * Gets the Two-Factor Auth provider for the specified|current user. * * @param WP_User $user Optional. User ID, or WP_User object of the user. Defaults to current user. * * @return string * @since 0.1-dev */ public static function get_primary_provider_for_user(WP_User $user): string { $loader = Rsssl_Provider_Loader::get_loader(); $available_providers = $loader::get_configured_providers_for_user($user); // If there's only one available provider, force that to be the primary. if (empty($available_providers)) { return ''; } if (1 === count($available_providers)) { $provider = key($available_providers); } else { $provider = Rsssl_Provider_Loader::get_user_enabled_providers($user); // Check if already a provider is active. // If the provider specified isn't enabled, just grab the first one that is based on the Weight. $best_valued_provider = 'totp'; if (isset($available_providers[$best_valued_provider]) && $available_providers[$best_valued_provider]::is_enabled($user)) { $provider = $best_valued_provider; } else { $provider = key($available_providers); } } return get_class($available_providers[$provider]) ?? ''; } /** * Quick boolean check for whether a given user is using two-step. * TODO: No longer needed? * * @param WP_User $user Optional. User ID, or WP_User object of the user. Defaults to current user. * * @return bool * @since 0.1-dev */ public static function is_user_using_two_factor(WP_User $user): bool { $provider = self::get_primary_provider_for_user($user); $enabled_providers_meta = Rsssl_Provider_Loader::get_user_enabled_providers($user); // Initialize as empty arrays if they are empty. $two_fa_forced_roles = rsssl_get_option('two_fa_forced_roles'); $two_fa_optional_roles = rsssl_get_option('two_fa_enabled_roles_email'); $two_fa_optional_roles_totp = rsssl_get_option('two_fa_enabled_roles_totp'); //ensure an array for all. if (!is_array($two_fa_forced_roles)) { $two_fa_forced_roles = []; } if (!is_array($two_fa_optional_roles)) { $two_fa_optional_roles = []; } if (!is_array($two_fa_optional_roles_totp)) { $two_fa_optional_roles_totp = []; } $two_fa_optional_roles = array_unique(array_merge($two_fa_optional_roles, $two_fa_optional_roles_totp)); foreach ($enabled_providers_meta as $enabled_provider) { $status = $enabled_provider::get_status($user); if ( ( 'disabled' === $status ) && is_object( $provider ) && get_class( $provider ) === $enabled_provider ) { $provider = []; } if ('active' === $status ) { return true; } if ('open' === $status) { return true; } } foreach ($user->roles as $role) { // If not forced, and not optional, or disabled, or provider not enabled. if (!in_array($role, $two_fa_forced_roles, true) && !in_array($role, $two_fa_optional_roles, true) ) { // Skip 2FA. return false; } } return !empty($provider); } /** * Show an expired onboarding error message. * * @param WP_Error $errors Error object to add the error to. * * @return WP_Error The updated error object. */ public static function show_expired_onboarding_error(WP_Error $errors): WP_Error { if ( isset( $_GET['nonce'], $_GET['errors'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'rsssl_expired' ) && $_GET['errors'] === 'expired' ) { $errors->add('expired', __('Your 2FA grace period expired. Please contact your site administrator to regain access and to configure 2FA.', 'really-simple-ssl')); } return $errors; } /** * Handle the browser-based login. * * @param string $user_login Username. * @param WP_User $user WP_User object of the logged-in user. * * @throws Exception If the onboarding process fails. * @since 0.1-dev */ public static function rsssl_wp_login(string $user_login, WP_User $user): void { switch (Rsssl_Two_Factor_Settings::get_login_action($user->ID)) { case 'onboarding': wp_clear_auth_cookie(); self::is_onboarding_complete($user); exit; case 'expired': // Destroy the current session for the user. self::destroy_current_session_for_user($user); wp_clear_auth_cookie(); self::display_expired_onboarding_error(); exit; case 'totp': case 'email': case 'passkey': wp_clear_auth_cookie(); self::show_two_factor_login($user); exit; case 'login': default: break; } } /** * Destroy the known password-based authentication sessions for the current user. * * Is there a better way of finding the current session token without * having access to the authentication cookies which are just being set * on the first password-based authentication request. * * @param WP_User $user User object. * * @return void */ public static function destroy_current_session_for_user(WP_User $user): void { $session_manager = WP_Session_Tokens::get_instance($user->ID); foreach (self::$password_auth_tokens as $auth_token) { $session_manager->destroy($auth_token); } } /** * Prevent login through XML-RPC and REST API for users with at least one * two-factor method enabled. * * @param WP_User|WP_Error $user Valid WP_User only if the previous filters * have verified and confirmed the * authentication credentials. * * @return WP_User|WP_Error */ public static function rsssl_filter_authenticate($user) { if ($user instanceof WP_User && self::is_api_request() && self::is_user_using_two_factor($user) && !self::is_user_api_login_enabled($user->ID)) { return new WP_Error( 'invalid_application_credentials', __('API login for user disabled.', 'really-simple-ssl') ); } return $user; } /** * Prevent login cookies being set on login for Two Factor users. * * This makes it so that Core never sends the auth cookies. `login_form_validate_2fa()` will send them manually once the 2nd factor has been verified. * * @param WP_User|WP_Error $user Valid WP_User only if the previous filters * have verified and confirmed the * authentication credentials. * * @return WP_User|WP_Error */ public static function rsssl_filter_authenticate_block_cookies($user) { /* * NOTE: The `login_init` action is checked for here to ensure we're within the regular login flow, * rather than through an unsupported 3rd-party login process which this plugin doesn't support. */ if ($user instanceof WP_User && self::is_user_using_two_factor($user) && did_action('login_init')) { add_filter('send_auth_cookies', '__return_false', PHP_INT_MAX); } return $user; } /** * If the current user can log in via API requests such as XML-RPC and REST. * * @param integer $user_id User ID. * * @return boolean */ public static function is_user_api_login_enabled(int $user_id): bool { return (bool)apply_filters('rsssl_two_factor_user_api_login_enable', false, $user_id); } /** * Is the current request an XML-RPC or REST request. * * @return boolean */ public static function is_api_request(): bool { if (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST) { return true; } if (defined('REST_REQUEST') && REST_REQUEST) { return true; } return false; } /** * Display the login form. * * @param WP_User $user WP_User object of the logged-in user. * * @throws Exception If the login nonce creation fails. * @since 0.1-dev */ public static function show_two_factor_login(WP_User $user): void { $redirect_to = isset($_REQUEST['redirect_to']) ? wp_validate_redirect(wp_unslash($_REQUEST['redirect_to']), admin_url()) : admin_url(); $provider = Rsssl_Two_Factor_Settings::get_login_action($user->ID); $login_nonce = Rsssl_Two_Fa_Authentication::create_login_nonce($user->ID)['rsssl_key']; self::login_html($user, $login_nonce ,$redirect_to); } /** * Displays a message informing the user that their account has had failed login attempts. * * @param WP_User $user WP_User object of the logged-in user. */ public static function maybe_show_last_login_failure_notice(WP_User $user): void { $last_failed_two_factor_login = (int)get_user_meta($user->ID, self::RSSSL_USER_RATE_LIMIT_KEY, true); $failed_login_count = (int)get_user_meta( $user->ID, self::RSSSL_USER_FAILED_LOGIN_ATTEMPTS_KEY, true ); if ($last_failed_two_factor_login) { echo '<div id="login_notice" class="message"><strong>'; // translators: %1$s is the number of failed login attempts, %2$s is the time since the last failed login. printf( esc_html( _n( 'Warning: There has been %1$s failed login attempt on your account without providing a valid two-factor token. The last failed login occurred %2$s ago. If this wasn\'t you, you should reset your password.', 'Warning: %1$s failed login attempts have been detected on your account without providing a valid two-factor token. The last failed login occurred %2$s ago. If this wasn\'t you, you should reset your password.', $failed_login_count, 'really-simple-ssl' ) ), esc_html(number_format_i18n($failed_login_count)), esc_html(human_time_diff($last_failed_two_factor_login, time())) ); echo '</strong></div>'; } } /** * Show the password reset notice if the user's password was reset. * * They were also sent an email notification in `send_password_reset_email()`, but email sent from a typical * web server is not reliable enough to trust completely. * * @param WP_Error $errors The error object. * * @return WP_Error */ public static function rsssl_maybe_show_reset_password_notice(WP_Error $errors): WP_Error { if ('incorrect_password' !== $errors->get_error_code()) { return $errors; } if (!isset($_POST['log'])) { return $errors; } $user_name = sanitize_user(wp_unslash($_POST['log'])); $attempted_user = get_user_by('login', $user_name); if ( $user_name && ! $attempted_user && strpos( $user_name, '@') !== false ) { $attempted_user = get_user_by('email', $user_name); } if (!$attempted_user) { return $errors; } $password_was_reset = get_user_meta($attempted_user->ID, self::RSSSL_USER_PASSWORD_WAS_RESET_KEY, true); if (!$password_was_reset) { return $errors; } $errors->remove('incorrect_password'); $errors->add( 'rsssl_two_factor_password_reset', sprintf( /* translators: %s: URL to reset password */ __( 'Your password was reset because of too many failed Two Factor attempts. You will need to <a href="%s">create a new password</a> to regain access. Please check your email for more information.', 'really-simple-ssl' ), esc_url(add_query_arg('action', 'lostpassword', rsssl_wp_login_url())) ) ); return $errors; } /** * Clear the password reset notice after the user resets their password. * * @param WP_User $user WP_User object of the logged-in user. */ public static function rsssl_clear_password_reset_notice(WP_User $user): void { delete_user_meta($user->ID, self::RSSSL_USER_PASSWORD_WAS_RESET_KEY); } /** * Generates the html form for the second step of the authentication process. * * @param WP_User $user WP_User object of the logged-in user. * @param string $login_nonce A string nonce stored in usermeta. * @param string $redirect_to The URL to which the user would like to be redirected. * @param string $error_msg Optional. Login error message. * @param string|object $provider An override to the provider. * * @throws Exception If the login nonce creation fails. * @since 0.1-dev */ public static function login_html( WP_User $user, string $login_nonce, string $redirect_to, string $error_msg = '', $provider = null ): void { if (empty($provider)) { $provider = self::get_primary_provider_for_user($user); } elseif (is_string($provider) && method_exists($provider, 'get_instance')) { $provider = call_user_func(array($provider, 'get_instance')); } if (!$provider) { return; } $provider_class = $provider::get_instance(); $available_providers = self::get_available_providers_for_user($user); // $backup_providers = array_diff_key($available_providers, array($provider => null)); $interim_login = isset($_REQUEST['interim-login']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $rememberme = (int)self::rememberme(); if (!function_exists('login_header')) { // We really should migrate login_header() out of `wp-login.php` so it can be called from an includes file. include_once __DIR__ . '/function-login-header.php'; } // Enqueue two-fa JavaScript assets $uri = trailingslashit(rsssl_url) . 'assets/features/two-fa/assets.min.js'; $uri_file = trailingslashit(rsssl_path) . 'assets/features/two-fa/assets.min.js'; add_filter('wp_script_attributes', [self::class, 'handle_script_attributes'], 10, 2); wp_enqueue_script('rsssl-frontend-settings', $uri, array(), filemtime($uri_file), true); wp_localize_script('rsssl-frontend-settings', 'rsssl_validate', array( 'nonce' => wp_create_nonce('wp_rest'), 'root' => esc_url_raw(rest_url(self::REST_NAMESPACE)), 'login_nonce' => $login_nonce, 'redirect_to' => $redirect_to, 'user_id' => $user->ID, 'origin' => 'validation', 'translatables' => apply_filters('rsssl_two_factor_translatables', []), )); // Load the login template. rsssl_load_template( 'login.php', compact( 'login_nonce', 'redirect_to', 'error_msg', 'provider', // 'backup_providers', 'interim_login', 'rememberme', 'provider_class', 'user' ), rsssl_path . 'assets/templates/two_fa/' ); if (!function_exists('login_footer')) { include_once __DIR__ . '/function-login-footer.php'; } login_footer(); } /** * Generate the two-factor login form URL. * * @param array $params List of query argument pairs to add to the URL. * @param string $scheme URL scheme context. * * @return string */ public static function login_url(array $params = array(), string $scheme = 'login'): string { $params = urlencode_deep($params); return add_query_arg($params, site_url('wp-login.php', $scheme)); } /** * Determine the minimum wait between two factor attempts for a user. * * This implements an increasing backoff, requiring an attacker to wait longer * each time to attempt to brute-force the login. * * @param WP_User $user The user being operated upon. * * @return int Time delay in seconds between login attempts. */ public static function get_user_time_delay(WP_User $user): int { /** * Filter the minimum time duration between two factor attempts. * * @param int $rate_limit The number of seconds between two factor attempts. */ $rate_limit = apply_filters('rsssl_two_factor_rate_limit', 1); $user_failed_logins = get_user_meta($user->ID, self::RSSSL_USER_FAILED_LOGIN_ATTEMPTS_KEY, true); if ($user_failed_logins) { $rate_limit = (2 ** $user_failed_logins) * $rate_limit; /** * Filter the maximum time duration a user may be locked out from retrying two-factor authentications. * * @param int $max_rate_limit The maximum number of seconds a user might be locked out for. Default 15 minutes. */ $max_rate_limit = apply_filters('rsssl_two_factor_max_rate_limit', 15 * MINUTE_IN_SECONDS); $rate_limit = min($max_rate_limit, $rate_limit); } /** * Filters the per-user time duration between two-factor login attempts. * * @param int $rate_limit The number of seconds between two factor attempts. * @param WP_User $user The user attempting to log in. */ return apply_filters('rsssl_two_factor_user_rate_limit', $rate_limit, $user); } /** * Determine if a time delay between user two-factor login attempts should be triggered. * * @param WP_User $user The User. * * @return bool True if rate limit is okay, false if not. * @since 0.8.0 */ public static function is_user_rate_limited(WP_User $user): bool { $rate_limit = self::get_user_time_delay($user); $last_failed = get_user_meta($user->ID, self::RSSSL_USER_RATE_LIMIT_KEY, true); $rate_limited = false; if ($last_failed && $last_failed + $rate_limit > time()) { $rate_limited = true; } /** * Filter whether this login attempt is rate limited or not. * * This allows for dedicated plugins to rate limit two-factor login attempts * based on their own rules. * * @param bool $rate_limited Whether the user login is rate limited. * @param WP_User $user The user attempting to log in. */ return apply_filters('rsssl_two_factor_is_user_rate_limited', $rate_limited, $user); } /** * Validates the two-factor authentication code. for all providers. * * @return void * @throws Exception */ public static function rsssl_login_form_validate_2fa(): void { [$wp_auth_id, $nonce, $provider_key, $redirect_to] = self::get_request_data(); if (isset($_SERVER['REQUEST_METHOD']) && 'POST' === strtoupper((sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD']))))) { $is_post_request = true; } else { $is_post_request = false; } if (!$wp_auth_id || !$nonce) { return; } $user = get_userdata($wp_auth_id); if (!$user) { return; } // Verify the nonce if (true !== Rsssl_Two_Fa_Authentication::verify_login_nonce($user->ID, $nonce)) { wp_safe_redirect(home_url()); exit; } $loader = Rsssl_Provider_Loader::get_loader(); // Get the provider $providers = $loader::get_enabled_providers_for_user($user); if ($provider_key && isset($providers[$provider_key])) { $provider_class = get_class($providers[$provider_key]); } else { wp_die(esc_html__('Authentication provider not specified or invalid.', 'really-simple-ssl'), 403); } /** @var Rsssl_Two_Factor_Provider $provider_instance */ $provider_instance = $provider_class::get_instance(); // Check for corrupted/empty TOTP key before attempting authentication self::validate_totp_key_exists( $user, $provider_key ); // Allow the provider to re-send codes, etc. if ( ( 'email' === $provider_key ) && true === $provider_instance->pre_process_authentication( $user ) ) { // Always generate a new nonce. $new_nonce = self::generate_login_nonce_for_user($user->ID); self::login_html($user, $new_nonce, $redirect_to, '', $provider_class); exit; } // If the form hasn't been submitted, just display the auth form. if (!$is_post_request) { self::handle_not_post_request($user, $provider_class); exit; } if (self::is_user_rate_limited($user)) { $time_delay = self::get_user_time_delay($user); $last_failed = get_user_meta($user->ID, self::RSSSL_USER_RATE_LIMIT_KEY, true); $error = new WP_Error( 'rsssl_two_factor_too_fast', sprintf( /* translators: %s: time delay between login attempts */ __( 'Too many invalid verification codes, you can try again in %s. This limit protects your account against automated attacks.', 'really-simple-ssl' ), human_time_diff($last_failed + $time_delay) ) ); do_action('rsssl_wp_login_failed', $user->user_login, $error); // Display the login form with an error message self::login_html( $user, $redirect_to, esc_html($error->get_error_message()), $provider_key ); exit; } // Validate authentication if (!$provider_instance->validate_authentication($user)) { // Handle rate limiting and failed attempts self::handle_failed_attempt($user, $provider_class, $redirect_to, $nonce); exit; } // Successful authentication self::complete_authentication($user, $redirect_to); } /** * Handles the case when a two-factor authentication attempt fails. * * * @return void * @throws Exception */ protected static function handle_failed_attempt(WP_User $user, string $provider_class, string $redirect_to, string $login_nonce): void { // Store the last time a failed login occurred. update_user_meta($user->ID, self::RSSSL_USER_RATE_LIMIT_KEY, time()); // Store the number of failed login attempts. update_user_meta( $user->ID, self::RSSSL_USER_FAILED_LOGIN_ATTEMPTS_KEY, 1 + (int)get_user_meta($user->ID, self::RSSSL_USER_FAILED_LOGIN_ATTEMPTS_KEY, true) ); if (self::should_reset_password($user->ID)) { self::reset_compromised_password($user); self::send_password_reset_emails($user); self::show_password_reset_error(); exit; } /** @var Rsssl_Two_Factor_Provider_Interface $provider_class */ $provider_class::get_instance(); self::login_html( $user, $login_nonce, $redirect_to, esc_html__('Invalid verification code.', 'really-simple-ssl'), $provider_class ); } /** * Completes the two-factor authentication process. After a successful authentication, the user is redirected to the appropriate page. * * @return void */ protected static function complete_authentication(WP_User $user, string $redirect_to): void { $rememberme = false; if (isset($_REQUEST['rememberme']) && filter_var(wp_unslash($_REQUEST['rememberme']), FILTER_VALIDATE_BOOLEAN)) { $rememberme = true; } // Authenticate the user. wp_set_auth_cookie($user->ID, $rememberme); do_action('rsssl_two_factor_user_authenticated', $user); $redirect_to = apply_filters('login_redirect', $redirect_to, $redirect_to, $user); // cleaning up the user meta. delete_user_meta( $user->ID, self::RSSSL_USER_FAILED_LOGIN_ATTEMPTS_KEY); delete_user_meta( $user->ID, self::RSSSL_USER_RATE_LIMIT_KEY); wp_safe_redirect($redirect_to); exit; } /** * Handle the case when the request method is not POST. * * @param WP_User $user The user object. * @param string $provider The provider name. * * @return void * @throws Exception If the login nonce cannot be created. */ private static function handle_not_post_request(WP_User $user, string $provider): void { $login_nonce = self::generate_login_nonce_for_user($user->ID); self::login_html( $user, $login_nonce, isset($_REQUEST['redirect_to']) ? wp_validate_redirect(wp_unslash($_REQUEST['redirect_to']), '') : '', '', $provider ); } /** * Get the request data for two-factor authentication. * * @return array An array containing the sanitized values of wp_auth_id, nonce, and provider. */ private static function get_request_data(): array { $wp_auth_id = self::sanitize_request_data('rsssl-wp-auth-id', 0, 'absint'); $nonce = self::sanitize_request_data('rsssl-wp-auth-nonce', '', 'wp_unslash'); $provider = self::sanitize_request_data('provider', false, 'wp_unslash'); $redirect_to = self::sanitize_request_data('redirect_to', '', 'wp_unslash'); return array($wp_auth_id, $nonce, $provider, $redirect_to); } /** * Sanitize request data. * * @param string $key The key to retrieve from the $_REQUEST array. * @param mixed $default_value The default value to return if the key does not exist in the $_REQUEST array. * @param callable $sanitize_callback The callback function used to sanitize the value. * * @return mixed The sanitized value if it exists in the $_REQUEST array, otherwise the default value. */ private static function sanitize_request_data(string $key, $default_value, callable $sanitize_callback) { return !empty($_REQUEST[$key]) ? $sanitize_callback(sanitize_text_field(wp_unslash($_REQUEST[$key]))) : $default_value; } /** * Checks if a user's password should be reset based on the number of failed login attempts on the 2nd factor. * * @param int $user_id The ID of the user. * * @return bool True if the password should be reset, false otherwise. */ public static function should_reset_password(int $user_id): bool { $failed_attempts = (int)get_user_meta($user_id, self::RSSSL_USER_FAILED_LOGIN_ATTEMPTS_KEY, true); /** * Filters the maximum number of failed attempts on a 2nd factor before the user's * password will be reset. After a reasonable number of attempts, it's safe to assume * that the password has been compromised and an attacker is trying to brute force the 2nd * factor. * * ⚠️ `get_user_time_delay()` mitigates brute force attempts, but many 2nd factors -- * like TOTP and backup codes -- are very weak on their own, so it's not safe to give * attackers unlimited attempts. Setting this to a very large number is strongly * discouraged. * * @param int $limit The number of attempts before the password is reset. */ $failed_attempt_limit = apply_filters('rsssl_two_factor_failed_attempt_limit', 30); return $failed_attempts >= $failed_attempt_limit; } /** * Reset a compromised password. * * If we know that the password is compromised, we have the responsibility to reset it and inform the * user. `get_user_time_delay()` mitigates brute force attempts, but this acts as an extra layer of defense * which guarantees that attackers can't brute force it (unless they compromise the new password). * * @param WP_User $user The user who failed to log in. */ public static function reset_compromised_password(WP_User $user): void { // Unhook because `wp_password_change_notification()` wouldn't notify the site admin when // their password is compromised. remove_action('after_password_reset', 'wp_password_change_notification'); reset_password($user, wp_generate_password(25)); update_user_meta($user->ID, self::RSSSL_USER_PASSWORD_WAS_RESET_KEY, true); add_action('after_password_reset', 'wp_password_change_notification'); Rsssl_Two_Fa_Authentication::delete_login_nonce($user->ID); delete_user_meta($user->ID, self::RSSSL_USER_RATE_LIMIT_KEY); delete_user_meta($user->ID, self::RSSSL_USER_FAILED_LOGIN_ATTEMPTS_KEY); } /** * Notify the user and admin that a password was reset for being compromised. * * @param WP_User $user The user whose password should be reset. */ public static function send_password_reset_emails(WP_User $user): void { self::notify_user_password_reset($user); /** * Filters whether to email the site admin when a user's password has been * compromised and reset. * * @param bool $reset `true` to notify the admin, `false` to not notify them. */ $notify_admin = apply_filters('rsssl_two_factor_notify_admin_user_password_reset', true); $admin_email = get_option('admin_email'); if ($notify_admin && $admin_email !== $user->user_email) { self::notify_admin_user_password_reset($user); } } /** * Show the password reset error when on the login screen. */ public static function show_password_reset_error(): void { $error = new WP_Error( 'too_many_attempts', sprintf( '<p>%s</p> <p style="margin-top: 1em;">%s</p>', __( 'There have been too many failed two-factor authentication attempts, which often indicates that the password has been compromised. The password has been reset in order to protect the account.', 'really-simple-ssl' ), __( 'If you are the owner of this account, please check your email for instructions on regaining access.', 'really-simple-ssl' ) ) ); login_header(__('Password Reset', 'really-simple-ssl'), '', $error); login_footer(); } /** * Should the login session persist between sessions. * * @return boolean */ public static function rememberme(): bool { $rememberme = false; if (!empty($_REQUEST['rememberme'])) { $rememberme = true; } return (bool)apply_filters('rsssl_two_factor_rememberme', $rememberme); } /** * Check if the user has completed the onboarding process. * * @param WP_User $user The WP_User object representing the user. * * @return void * @throws Exception If the onboarding screen template cannot be loaded. */ private static function is_onboarding_complete(WP_User $user): void { // If the user has not completed the onboarding process, they should be shown the onboarding screen. $onboarding_complete = get_user_meta($user->ID, self::RSSSL_USER_META_ONBOARDING_COMPLETE, true); if (!$onboarding_complete) { self::onboarding_user_html($user); } } /** * Display the expired onboarding error. Manually load our login header and * footer functions to ensure they are available. */ private static function display_expired_onboarding_error(): void { if (!function_exists('login_header')) { include_once __DIR__ . '/function-login-header.php'; } if (!function_exists('login_footer')) { include_once __DIR__ . '/function-login-footer.php'; } rsssl_load_template('expired.php', [ 'message' => esc_html__('Your 2FA grace period expired. Please contact your site administrator to regain access and to configure 2FA.', 'really-simple-ssl'), ], rsssl_path . 'assets/templates/two_fa/'); } /** * Validate that TOTP key exists for the user when TOTP provider is used. * Destroys session and displays error if key is corrupted/missing. * * @param WP_User $user The user object. * @param string $provider_key The provider key being used. * * @return void */ private static function validate_totp_key_exists( WP_User $user, string $provider_key ): void { if ( 'totp' !== $provider_key ) { return; } if ( ! class_exists( 'RSSSL\Pro\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Totp' ) ) { return; } $totp_key = get_user_meta( $user->ID, \RSSSL\Pro\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Totp::SECRET_META_KEY, true ); if ( empty( $totp_key ) ) { // Verify we have a valid user before destroying their session if ( ! $user instanceof WP_User || ! $user->exists() ) { wp_die( esc_html__( 'Invalid user.', 'really-simple-ssl' ), 403 ); } // TOTP key is missing/corrupted self::destroy_current_session_for_user( $user ); wp_clear_auth_cookie(); self::display_corrupted_totp_error(); exit; } } /** * Display error when TOTP key is corrupted/missing. Manually load our login header and * footer functions to ensure they are available. * Follows the same template as the expired onboarding error. */ private static function display_corrupted_totp_error(): void { if (!function_exists('login_header')) { include_once __DIR__ . '/function-login-header.php'; } if (!function_exists('login_footer')) { include_once __DIR__ . '/function-login-footer.php'; } rsssl_load_template('expired.php', [ 'message' => esc_html__('Your Two-Factor Authentication configuration is corrupted. Please contact your site administrator to regain access.', 'really-simple-ssl'), ], rsssl_path . 'assets/templates/two_fa/'); } /** * Generate the HTML for the onboarding screen for a given user. * * @param WP_User $user The user object. * * @return void * @throws Exception If the onboarding screen template cannot be loaded. */ private static function onboarding_user_html(WP_User $user): void { $passkey_onboarding = get_user_meta($user->ID, 'rsssl_two_fa_status_passkey', true) === 'open'; // Variables needed for the template and scripts $onboarding_url = self::login_url(array('action' => 'rsssl_onboarding'), 'login_post'); $provider_loader = Rsssl_Provider_Loader::get_loader(); $provider = self::get_primary_provider_for_user($user); $redirect_to = isset($_REQUEST['redirect_to']) ? wp_validate_redirect(wp_unslash($_REQUEST['redirect_to']), admin_url()) : admin_url(); $enabled_providers = $provider_loader::get_user_enabled_providers($user); $login_nonce = self::generate_login_nonce_for_user($user->ID); $is_forced = Rsssl_Two_Factor_Settings::is_user_forced_to_use_2fa($user->ID); $grace_period = Rsssl_Two_Factor_Settings::is_user_in_grace_period($user); $is_today = Rsssl_Two_Factor_Settings::is_today($user); if ($passkey_onboarding) { $is_forced = false; //if only passkey is available, set it as the only provider if (count($enabled_providers) === 1 && isset($enabled_providers['passkey'])) { $provider = 'passkey'; } } // Ensure login_header and login_footer functions are available if (!function_exists('login_header')) { include_once __DIR__ . '/function-login-header.php'; } if (!function_exists('login_footer')) { include_once __DIR__ . '/function-login-footer.php'; } //Add the styles for the two-factor authentication. add_action('login_enqueue_styles', array(__CLASS__, 'enqueue_onboarding_styles')); $uri = trailingslashit(rsssl_url) . 'assets/features/two-fa/assets.min.js'; $uri_file = trailingslashit(rsssl_path) . 'assets/features/two-fa/assets.min.js'; add_filter('wp_script_attributes', [self::class, 'handle_script_attributes'], 10, 2); wp_enqueue_script('rsssl-frontend-settings', $uri, array(), filemtime($uri_file), true); wp_localize_script('rsssl-frontend-settings', 'rsssl_onboard', array( 'nonce' => wp_create_nonce('wp_rest'), 'root' => esc_url_raw(rest_url(self::REST_NAMESPACE)), 'login_nonce' => $login_nonce, 'redirect_to' => $redirect_to, 'user_id' => $user->ID, 'origin' => 'onboarding', 'translatables' => apply_filters('rsssl_two_factor_translatables', []), )); login_header( __('Two-Factor Authentication Setup', 'really-simple-ssl'), '', null ); rsssl_load_template( 'onboarding.php', array( 'user' => $user, 'login_nonce' => $login_nonce, 'url' => $onboarding_url, 'provider' => $provider, 'redirect_to' => $redirect_to, 'available_providers' => $enabled_providers, 'interim_login' => isset($_REQUEST['interim-login']), 'rememberme' => (int)self::rememberme(), 'primary_provider' => $provider, 'is_forced' => $is_forced, 'grace_period' => $grace_period, 'is_today' => $is_today, 'skip_two_fa_url' => Rsssl_Two_Factor_Settings::rsssl_one_time_login_url($user->ID), ), rsssl_path . 'assets/templates/two_fa/' ); wp_enqueue_script('rsssl-rest-settings'); login_footer(); if (ob_get_level() > 0) { ob_flush(); } flush(); exit; //This was the original exit. } /** * Handles the script attributes. * * * @param array $attributes * @param string $handle * * @return array */ public static function handle_script_attributes( array $attributes, string $handle = ''):array { if ( $handle === 'rsssl-profile-settings' ) { $attributes['type'] = 'module'; } return $attributes; } /** * Enqueues the RSSSL profile settings stylesheet. * * @return void */ public static function enqueue_onboarding_styles(): void { $url = trailingslashit(rsssl_url) . 'assets/features/two-fa/styles.css'; $file = trailingslashit(rsssl_path) . 'assets/features/two-fa/styles.css'; wp_enqueue_style('rsssl-profile-settings', $url, array(), filemtime($file)); } /** * Return the translatable strings for the two-factor authentication. * @return array */ public static function translatables(): array { return self::rsssl_translatables([]); } /** * places all translatable strings. * * * @return array */ public static function rsssl_translatables(array $translatables): array { $new_translatables = [ 'download_codes' => esc_html__('Download Backup Codes', 'really-simple-ssl'), 'keyCopied' => __('Key copied', 'really-simple-ssl'), 'keyCopiedFailed' => __('Could not copy text: ', 'really-simple-ssl'), ]; return array_merge($translatables, $new_translatables); } /** * Generates a login nonce for a user. and returns the key. * * @param $user_id * * @return string */ protected static function generate_login_nonce_for_user( $user_id ): string { $login_nonce = Rsssl_Two_Fa_Authentication::create_login_nonce( $user_id ); if ( ! $login_nonce ) { $error = new WP_Error(); $error->add( 'login_nonce_creation_failed', __( 'Failed to create a login nonce.', 'really-simple-ssl' ) ); } return $login_nonce['rsssl_key']; } } /** * Hook as soon as the file is required. Which is the plugins_loaded hook. * @see security/integrations.php */ $rsssl_two_factor_compat = new Rsssl_Two_Factor_Compat(); Rsssl_Two_Factor::add_hooks($rsssl_two_factor_compat); wordpress/two-fa/class-rsssl-passkey-list-table.php 0000777 00000012224 15251751331 0016501 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa; use RSSSL\Pro\Security\WordPress\Passkey\Rsssl_Public_Credential_Resource; use WP_List_Table; if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } class RSSSL_Passkey_List_Table extends WP_List_Table { public function __construct() { parent::__construct([ 'singular' => __('Device', 'really-simple-ssl'), 'plural' => __('Devices', 'really-simple-ssl'), 'ajax' => true ]); } /** * Get the columns * @return array */ public function get_columns(): array { return [ 'device_name' => __('Device Name', 'really-simple-ssl'), 'registered' => __('Registered', 'really-simple-ssl'), 'last_used' => __('Last Used', 'really-simple-ssl'), 'actions' => __('Actions', 'really-simple-ssl') ]; } /** * Prepare the items * * @return void */ public function prepare_items(array $data = []) :void { $columns = $this->get_columns(); $hidden = []; $sortable = [ 'registered' => ['registered', false], 'last_used' => ['last_used', false] ]; $this->_column_headers = [$columns, $hidden, $sortable]; $this->items = $data; // Assigning data to be used in display_rows() } /** * Default column value * * @param array $item * @param string $column_name * * @return string */ public function column_default($item, $column_name): string { switch ($column_name) { case 'device_name': return esc_html($item['device_name']); case 'registered': return esc_html($item['registered']); case 'last_used': return esc_html($item['last_used']); case 'actions': return sprintf( '<form method="post" class="rsssl-remove-passkey-form" style="display:inline;"> <input type="hidden" name="device_id" value="%s" /> <button type="button" class="button rsssl-remove-passkey" data-device-id="%s">%s</button> </form>', esc_attr($item['id']), esc_attr($item['id']), esc_html__('Remove', 'really-simple-ssl') ); default: return print_r($item, true); } } /** * Display the rows or placeholder * @return void */ public function display_rows_or_placeholder(): void { if (!empty($this->items)) { echo '<tbody id="rsssl-passkey-list">'; $this->display_rows(); } else { echo '<tbody id="rsssl-passkey-list" class="no-items">'; } echo '</tbody>'; } /** * Display the table navigation * @param $which * * @return void */ public function display_tablenav($which): void { if ('top' === $which) { echo '<div class="passkey-datatable">'; echo '<h1 class="passkey-datatable-title">' . esc_html__('Passkeys', 'really-simple-ssl') . '</h1>'; echo '<a id="rsssl-add-passkey-button" data-skip_redirect="true" class="button passkey-registration-button">' . esc_html__('Add Device', 'really-simple-ssl') . '</a>'; echo '</div>'; } } /** * Display the table * @return void */ public function display(): void { $this->display_tablenav('top'); echo '<table class="wp-list-table ' . implode(' ', $this->get_table_classes()) . '">'; $this->display_header(); $this->display_rows_or_placeholder(); $this->display_footer(); echo '</table>'; $this->display_tablenav('bottom'); } /** * Display the header of the table * @return void */ protected function display_header(): void { echo '<thead>'; $this->print_column_headers(); echo '</thead>'; } /** * Display the footer of the table * @return void */ protected function display_footer(): void { echo '<tfoot>'; $this->print_column_headers(false); echo '</tfoot>'; } /** * Display the passkey table * * @return void */ public static function display_table(array $data = []): void { $list_table = new self(); $list_table->prepare_items($data); $list_table->display(); } } add_action('wp_ajax_remove_passkey', 'remove_passkey_callback'); /** * Remove passkey callback * * @return void */ function remove_passkey_callback() { $device_id = isset($_POST['device_id']) ? (int) $_POST['device_id'] : 0; if ($device_id > 0) { $resource = Rsssl_Public_Credential_Resource::get_instance(); if (is_null($resource)) { wp_send_json_error(['message' => __('Resource not found', 'really-simple-ssl')]); return; } $resource->delete($device_id); wp_send_json_success(['message' => __('Device removed successfully', 'really-simple-ssl')]); } else { wp_send_json_error(['message' => __('Invalid device ID', 'really-simple-ssl')]); } } wordpress/two-fa/repositories/class-rsssl-two-fa-user-repository.php 0000777 00000020704 15251751331 0022103 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Repositories; use RSSSL\Security\WordPress\Two_Fa\Contracts\Rsssl_Two_Fa_User_Repository_Interface; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_Factor_User_Factory; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_Fa_User_Collection; use WP_User_Query; class Rsssl_Two_Fa_User_Repository implements Rsssl_Two_Fa_User_Repository_Interface { /** @var Rsssl_Two_Factor_User_Factory */ private Rsssl_Two_Factor_User_Factory $factory; /** * Constructor. */ public function __construct() { $this->factory = new Rsssl_Two_Factor_User_Factory(); } /** * Helper to build and fetch a user collection with a chain of builder operations. * * @param Rsssl_Two_FA_Data_Parameters $params * @param callable $chain Rsssl_Two_Fa_User_Query_Builder $chain * @return Rsssl_Two_Fa_User_Collection */ private function fetchBy(Rsssl_Two_FA_Data_Parameters $params, callable $chain): Rsssl_Two_Fa_User_Collection { $builder = new Rsssl_Two_Fa_User_Query_Builder($params); $chain($builder); return $this->buildUserCollection($builder->getArgs(), $params); } /** * Retrieve two-factor authentication users based on the provided parameters. */ public function getTwoFaUsers(Rsssl_Two_FA_Data_Parameters $params): Rsssl_Two_Fa_User_Collection { // we check if there is a rolesFilter set. $filter = false; if ( ! empty( $params->filter_value ) && $params->filter_value !== 'all' ) { // we have a roles filter set, so we add it to the params. $filter = true; } return $this->fetchBy($params, fn($b) => $b ->addRolesFilter($filter) ); } /** * Retrieve two-factor authentication users that are considered "expired." * * Expiration is determined by comparing the user's last login to a threshold date, * and only users with a last login older than that threshold (plus the two-factor * status conditions) are returned. */ public function geTwoFAExpiredUsers(Rsssl_Two_FA_Data_Parameters $params): Rsssl_Two_Fa_User_Collection { return $this->fetchBy($params, fn($b) => $b->addExpiredAndTwoFA()); } /** * Retrieve two-factor authentication users that are disabled. */ public function getTwoFaDisabledUsers(Rsssl_Two_FA_Data_Parameters $params): Rsssl_Two_Fa_User_Collection { return $this->fetchBy($params, fn($b) => $b->addDisabled()); } /** * Execute the WP_User_Query with the given arguments and convert the results * to a Rsssl_Two_Fa_User_Collection. */ private function buildUserCollection(array $args, Rsssl_Two_FA_Data_Parameters $params): Rsssl_Two_Fa_User_Collection { $collection = new Rsssl_Two_Fa_User_Collection(); $enabledRoles = $params->getEnabledRoles(); if ( empty( $enabledRoles ) ) { // we have no enabled roles, so we cannot query users return $collection; } // 1) Gather raw WP_User results, either network-wide or single-site if ( is_multisite() ) { $args = $this->buildMultiSiteBaseQuery($args, $params); } else { // single site installation $args = $this->buildSingleSiteBaseQuery($args, $params); } $query = new WP_User_Query( $args ); $results = $query->get_results(); $total = $query->get_total(); // 2) Set total records and bail early if no users $collection->setTotalRecords( $total ); if ( empty( $results ) ) { return $collection; } // 3) Map WP_User → TwoFA user objects exactly as before $forcedRoles = $params->getForcedRoles(); $enabledRoles = $params->getEnabledRoles(); $daysThreshold = $params->getDaysThreshold(); foreach ( $results as $user ) { $wpUser = get_userdata( $user->ID ); if ( ! $wpUser instanceof \WP_User ) { // If the user is not a WP_User instance, skip to the next iteration. continue; } $twoFaUser = $this->factory->createFromWPUser( $wpUser, $forcedRoles, $enabledRoles, $daysThreshold ); if ( $twoFaUser !== null && array_intersect($twoFaUser->getRoles(), $enabledRoles) ) { $collection->add( $twoFaUser ); } } return $collection; } /** * Build the base WP_User_Query for single-site installations. */ private function buildSingleSiteBaseQuery(array $args, Rsssl_Two_FA_Data_Parameters $params): array { // Ensure we only look at the current blog and keep the query lean. $args['blog_id'] = get_current_blog_id(); $args['fields'] = [ 'ID' ]; $args['count_total'] = true; return $args; } /** * Build the base WP_User_Query for multi-site installations. */ private function buildMultiSiteBaseQuery(array $args, Rsssl_Two_FA_Data_Parameters $params): array { global $wpdb; // Query users across the entire network (ignore site membership constraint). // `blog_id` = 0 makes WP_User_Query ignore per-site membership filtering in multisite. $args['blog_id'] = 0; $args['fields'] = [ 'ID' ]; $args['count_total'] = true; // Collect role filters (if any) that may have been added by the builder. $roles = []; if ( isset( $args['role'] ) && $args['role'] ) { $roles[] = $args['role']; unset( $args['role'] ); } if ( isset( $args['role__in'] ) && is_array( $args['role__in'] ) ) { $roles = array_merge( $roles, $args['role__in'] ); unset( $args['role__in'] ); } $roles = array_values( array_unique( array_filter( $roles ) ) ); // If we have role constraints, translate them to a meta_query that checks ANY site's // capabilities usermeta. For the main site the meta key is `${base_prefix}capabilities`, // for subsites it is `${base_prefix}{$blog_id}_capabilities`. if ( ! empty( $roles ) ) { $site_ids = get_sites( [ 'fields' => 'ids' ] ); $outer = [ 'relation' => 'OR' ]; foreach ( $site_ids as $blog_id ) { $cap_key = ( (int) $blog_id === 1 ) ? $wpdb->base_prefix . 'capabilities' : $wpdb->base_prefix . (int) $blog_id . '_capabilities'; // A user matches this site if ANY of the requested roles is present. $per_site = [ 'relation' => 'OR' ]; foreach ( $roles as $role ) { $per_site[] = [ 'key' => $cap_key, // Serialized array contains the role name as a string key; LIKE is sufficient. 'value' => '"' . $role . '"', 'compare' => 'LIKE', ]; } $outer[] = $per_site; } if ( ! empty( $outer ) ) { if ( isset( $args['meta_query'] ) && is_array( $args['meta_query'] ) ) { // Combine with an existing meta_query (AND the existing with our OR block). $args['meta_query'] = [ 'relation' => 'AND', $args['meta_query'], $outer, ]; } else { $args['meta_query'] = $outer; } } } return $args; } /** * Retrieve forced two-factor authentication users with an "open" status. and nearing expiry. * within the forced roles. */ public function getForcedTwoFaUsersWithOpenStatus(Rsssl_Two_FA_Data_Parameters $params ): Rsssl_Two_Fa_User_Collection { return $this->fetchBy($params, fn($b) => $b ->addOpenStatus() ->addForcedRoles($params->getForcedRoles()) ->addNearingExpiry() ); } /** * Retrieve forced two-factor authentication users with an "open" status and within the changed roles. */ public function getAddedForcedTwoFaUsersWithOpenStatus(Rsssl_Two_FA_Data_Parameters $params, array $changedRoles): Rsssl_Two_Fa_User_Collection { return $this->fetchBy($params, fn($b) => $b ->addOpenStatus() ->addForcedRolesFor($changedRoles) ); } /** * Retrieve forced two-factor authentication users with disabled status. */ public function getForcedTwoFaUsersWithDisabledStatus(Rsssl_Two_FA_Data_Parameters $params, array $newForcedRoles): Rsssl_Two_Fa_User_Collection { return $this->fetchBy($params, fn($b) => $b ->addForcedRolesFor($newForcedRoles) ->addDisabled() ); } /** * Retrieve forced two-factor authentication users with disabled status. */ public function getForcedTwoFaUsersWithExpiredStatus(Rsssl_Two_FA_Data_Parameters $params, array $newForcedRoles): Rsssl_Two_Fa_User_Collection { return $this->fetchBy($params, fn($b) => $b ->addForcedRolesFor($newForcedRoles) ->addExpired() ); } } wordpress/two-fa/repositories/class-rsssl-two-fa-user-query-builder.php 0000777 00000033732 15251751331 0022462 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Repositories; use RSSSL\Security\WordPress\Two_Fa\Contracts\Rsssl_Two_Fa_User_Query_Builder_Interface; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters; class Rsssl_Two_Fa_User_Query_Builder implements Rsssl_Two_Fa_User_Query_Builder_Interface { private $wpdb; private Rsssl_Two_FA_Data_Parameters $params; private array $args; private array $statusKeys = [ 'rsssl_two_fa_status_email', 'rsssl_two_fa_status_totp', 'rsssl_two_fa_status_passkey' ]; public function __construct( Rsssl_Two_FA_Data_Parameters $params ) { global $wpdb; $this->wpdb = $wpdb; $this->params = $params; $this->args = $this->buildQueryArgs( $params ); } /** * Build query args based on data parameters. */ public function buildQueryArgs( Rsssl_Two_FA_Data_Parameters $params ): array { // $metaQuery = $this->buildRoleMetaQuery($params); $pagination = [ 'number' => $params->number, 'offset' => $params->offset, ]; $args = array_merge( $pagination, [ // 'meta_query' => $metaQuery, 'fields' => [ 'ID', 'user_login' ], ] ); $enabledRoles = $params->getEnabledRoles(); if ( ! empty( $enabledRoles ) ) { $args['role__in'] = $enabledRoles; } return $args; } /** * Build the meta query for the user role specifically for two_fa_users. * * @return array|array[] */ protected function buildRoleMetaQuery( Rsssl_Two_FA_Data_Parameters $params ): array { if ( 'all' !== $params->filter_value && 'user_role' === $params->filter_column ) { if ( ! $this->isValidRole( $params->filter_value, $params ) ) { // Return a query that will never match any user. return [ [ 'key' => 'non_existing_meta_key', 'value' => 'non_existing_value', 'compare' => '=' ] ]; } // Use 'OR' as in the original code. return [ 'relation' => 'OR', [ 'key' => $this->wpdb->prefix . 'capabilities', 'value' => sprintf( ':"%s";b:1;', $params->filter_value ), 'compare' => 'LIKE', ], ]; } // The "all" branch remains the same. $enabledRoles = $params->getEnabledRoles(); if ( empty( $enabledRoles ) ) { return []; } $queries = array_map( function ( $role ) { return [ 'key' => $this->wpdb->prefix . 'capabilities', 'value' => sprintf( ':"%s";b:1;', $role ), 'compare' => 'LIKE', ]; }, $enabledRoles ); return array_merge( [ 'relation' => 'OR' ], $queries ); } /** * Check if the role is valid. */ protected function isValidRole( string $role, Rsssl_Two_FA_Data_Parameters $params ): bool { return in_array( $role, $params->getEnabledRoles(), true ) || in_array( $role, $params->getForcedRoles(), true ); } /** * Add expired and two-factor authentication conditions to the query arguments. */ public function addExpiredAndTwoFA(): self { $threshold = $this->params->getDaysThreshold(); $this->args = $this->addExpiredAndTwoFAConditionsToArgs( $this->args, $threshold ); return $this; } /** * Add the expired condition and two-factor status conditions to the query arguments. * * @param array $args The base query arguments. * @param int $daysThreshold Number of days to determine expiration. * * @return array The modified query arguments. */ public function addExpiredAndTwoFAConditionsToArgs( array $args, int $daysThreshold ): array { $metaConditions = []; // Include any existing meta_query conditions. if ( ! empty( $args['meta_query'] ) ) { $metaConditions[] = $args['meta_query']; } // Append the expired condition. $metaConditions[] = $this->getExpiredCondition( $daysThreshold ); // Append the two-factor status conditions. $metaConditions = array_merge( $metaConditions, $this->getTwoFAStatusConditions() ); // Combine all meta conditions with an AND relation. $args['meta_query'] = array_merge( [ 'relation' => 'AND' ], $metaConditions ); return $args; } /** * Build and return an array of meta query conditions for two‑factor status keys. * * For each key, users will be matched if the meta key is either set to "open" or does not exist. * * @return array */ private function getTwoFAStatusConditions(): array { $conditions = []; foreach ( $this->statusKeys as $key ) { $conditions[] = [ 'relation' => 'OR', [ 'key' => $key, 'value' => 'open', 'compare' => '=', ], [ 'key' => $key, 'compare' => 'NOT EXISTS', ], ]; } return $conditions; } /** * Build and return the expired condition meta query. * * This condition checks that the user's 'rsssl_two_fa_last_login' date is older than the current time minus the threshold days. * * @param int $daysThreshold The number of days to subtract from now. * * @return array */ private function getExpiredCondition( int $daysThreshold ): array { $expiredDateValue = date( "Y-m-d H:i:s", strtotime( "-{$daysThreshold} days" ) ); return [ 'key' => 'rsssl_two_fa_last_login', 'value' => $expiredDateValue, 'compare' => '<', 'type' => 'DATETIME', ]; } public function addRolesFilter($filter = false): self { if ( $filter ) { $this->args = $this->addRoleFilterConditionToArgs( $this->args, $this->params->filter_value ); } return $this; } private function addRoleFilterConditionToArgs( array $args, string $role ): array { // We check the enabled roles to see if the role is valid. $enabledRoles = $this->params->getEnabledRoles(); if ( ! in_array( $role, $enabledRoles, true ) ) { // If the role is not valid, we set a condition that matches no users. $args['role__in'] = [ 'non_existing_role' ]; return $args; } $args['role__in'] = [ $role ]; return $args; } /** * Build and add the expired condition to the meta query. * * This condition checks that the user's 'rsssl_two_fa_last_login' date is older than the current time minus the threshold days. * * @param int $daysThreshold The number of days to subtract from now. * * @return array */ public function addExpiredCondition( array $args, int $daysThreshold ): array { $expiredDateValue = date( "Y-m-d H:i:s", strtotime( "-{$daysThreshold} days" ) ); $metaQuery[] = [ 'key' => 'rsssl_two_fa_last_login', 'value' => $expiredDateValue, 'compare' => '<', 'type' => 'DATETIME', ]; if ( isset( $args['meta_query'] ) ) { $args['meta_query'] = [ 'relation' => 'AND', $args['meta_query'], $metaQuery, ]; } else { $args['meta_query'] = $metaQuery; } return $args; } /** * Add disabled condition to the query arguments. */ public function addDisabled(): self { $this->args = $this->addDisabledConditionToArgs( $this->args ); return $this; } /** * Add the disabled condition to the query arguments. */ public function addDisabledConditionToArgs( array $args ): array { $disabledConditions = []; foreach ( $this->statusKeys as $key ) { $disabledConditions[] = [ 'key' => $key, 'value' => 'disabled', 'compare' => '=', ]; } // Build the new condition group for disabled status. $newConditionGroup = array_merge( [ 'relation' => 'OR' ], $disabledConditions ); if ( isset( $args['meta_query'] ) ) { // Merge the new conditions with the existing meta_query. // Here, we assume that both the existing conditions and the new disabled conditions must be true, // so we use 'AND' to combine them. $args['meta_query'] = [ 'relation' => 'AND', $args['meta_query'], // existing meta_query conditions $newConditionGroup, // new disabled conditions ]; } else { // If no meta_query exists, simply use the new condition group. $args['meta_query'] = $newConditionGroup; } return $args; } /** * Add open status condition to the query arguments. */ public function addOpenStatus(): self { $this->args = $this->excludeActiveUsers( $this->args ); return $this; } /** * Add condition to find users with unconfigured 2FA. * */ public function addUnconfigured2FAConditionToArgs( array $args ): array { $metaQuery = []; foreach ( $this->statusKeys as $key ) { $metaQuery[] = [ 'relation' => 'OR', [ 'key' => $key, 'value' => 'active', 'compare' => '!=' ], [ 'key' => $key, 'compare' => 'NOT EXISTS', ] ]; } // Use AND relation to ensure ALL methods are not active $args['meta_query'] = array_merge( [ 'relation' => 'AND' ], $metaQuery ); return $args; } /** * Add nearing expiry condition to the query arguments. * * @param int $reminderBeforeClosingPeriod * * @return $this */ public function addNearingExpiry( int $reminderBeforeClosingPeriod = 3 ): self { $threshold = $this->params->getDaysThreshold(); $this->args = $this->addNearingExpiryCondition( $this->args, $threshold, $reminderBeforeClosingPeriod ); return $this; } /** * Build and return the nearing expiry condition meta query. * * This condition checks that the user's 'rsssl_two_fa_last_login' date is such that * they have three days or less remaining in their grace period. * * Given a total grace period ($daysThreshold), this returns a condition that only * returns users whose last login is between: * - now - $daysThreshold (i.e. the point of expiry), and * - now - ($daysThreshold - 3) (i.e. when 3 days remain). * * @param int $daysThreshold The total number of days in the grace period. * * @return array */ public function addNearingExpiryCondition( array $args, int $daysThreshold, int $reminderBeforeClosingPeriod = 3 ): array { // if the $daysThreshold is smaller than the reminderBeforeClosingPeriod. // there is no longer a need to check for the reminderBeforeClosingPeriod if ( $daysThreshold <= $reminderBeforeClosingPeriod ) { return []; } // Calculate the lower and upper bounds for the last login date. // Lower bound: The earliest date (i.e. furthest in the past) a user can have logged in // without being already expired. $lowerBound = date( "Y-m-d H:i:s", strtotime( "-{$daysThreshold} days" ) ); // Upper bound: The date corresponding to when exactly three days remain. $upperBound = date( "Y-m-d H:i:s", strtotime( "-" . ( $daysThreshold - $reminderBeforeClosingPeriod ) . " days" ) ); $nearingExpiryCondition = [ 'key' => 'rsssl_two_fa_last_login', 'value' => [ $lowerBound, $upperBound ], 'compare' => 'BETWEEN', 'type' => 'DATETIME', ]; if ( isset( $args['meta_query'] ) ) { // Preserve existing meta_query and add our condition $args['meta_query'] = [ 'relation' => 'AND', $args['meta_query'], $nearingExpiryCondition, ]; } else { $args['meta_query'] = [ 'relation' => 'AND', $nearingExpiryCondition ]; } return $args; } /** * Add expired only condition to the query arguments. */ public function addExpired(): self { $threshold = $this->params->getDaysThreshold(); $this->args = $this->addExpiredCondition( $this->args, $threshold ); return $this; } /** * Add forced roles condition to the query arguments. */ public function addForcedRoles(): self { $forced = $this->params->getForcedRoles(); $this->args = $this->filterForcedRolesFromEnabledRoles( $this->args, $forced ); return $this; } /** * Add forced roles condition to the query arguments. */ public function addForcedRolesFor( array $forcedRoles ): self { $this->args = $this->addForcedRolesConditionToArgs( $this->args, $forcedRoles ); return $this; } /** * Filter Specific on the forced roles */ public function filterForcedRolesFromEnabledRoles( array $args, array $getForcedRoles ): array { // we filter the forced roles to only those that are enabled. $enabledRoles = $this->params->getEnabledRoles(); // We only keep the roles that are in both forced and enabled. $forcedRoles = array_intersect( $getForcedRoles, $enabledRoles ); if ( empty( $forcedRoles ) ) { // If there are no forced roles after filtering, we set a condition that matches no users. $args['role__in'] = [ 'non_existing_role' ]; return $args; } $args['role__in'] = $forcedRoles; return $args; } /** * retrieve the query arguments. */ public function getArgs(): array { return $this->args; } /** * Apply a list of fluent calls in order. * * e.g. $b->chain(['addOpenStatus','addDisabled']); * * @param string[] $methods * * @return $this */ public function chain( array $methods ): self { foreach ( $methods as $m ) { if ( ! method_exists( $this, $m ) ) { throw new \InvalidArgumentException( "Unknown chain step: $m" ); } $this->{$m}(); } return $this; } private function excludeActiveUsers( array $args ): array { if (empty($this->statusKeys)) { return $args; } $conditions = []; foreach ($this->statusKeys as $key) { // Voor deze key: ofwel niet 'active', ofwel niet aanwezig $conditions[] = [ 'relation' => 'OR', [ 'key' => $key, 'value' => 'active', 'compare' => '!=', ], [ 'key' => $key, 'compare' => 'NOT EXISTS', ], ]; } // Alle keys moeten voldoen → AND $newConditionGroup = [ 'relation' => 'AND', ...$conditions, ]; if (isset($args['meta_query'])) { $args['meta_query'] = [ 'relation' => 'AND', $args['meta_query'], $newConditionGroup, ]; } else { $args['meta_query'] = $newConditionGroup; } return $args; } private function addOpenStatusConditionToArgs( array $args ) { $openStatusConditions = []; foreach ( $this->statusKeys as $key ) { $openStatusConditions[] = [ 'key' => $key, 'value' => 'open', 'compare' => '=', ]; } $newConditionGroup = array_merge( [ 'relation' => 'OR' ], $openStatusConditions ); if ( isset( $args['meta_query'] ) ) { $args['meta_query'] = [ 'relation' => 'AND', $args['meta_query'], $newConditionGroup, ]; } else { $args['meta_query'] = $newConditionGroup; } var_dump( $args ); return $args; } public function addForcedRolesConditionToArgs( array $args, array $getForcedRoles ): array { return $args; } } wordpress/two-fa/class-rsssl-two-factor-profile-settings.php 0000777 00000052400 15251751331 0020345 0 ustar 00 <?php /** * Holds the logic for the profile page. * * @package REALLY_SIMPLE_SSL */ namespace RSSSL\Security\WordPress\Two_Fa; use Exception; use RSSSL\Pro\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Totp; use RSSSL\Pro\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Backup_Codes; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Provider_Loader; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Email; use RSSSL\Security\WordPress\Two_Fa\Traits\Rsssl_Two_Fa_Helper; use WP_User; if (!class_exists('Rsssl_Two_Factor_Profile_Settings')) { /** * Class Rsssl_Two_Factor_Profile_Settings * * This class is responsible for handling the Two-Factor Authentication settings on the user profile page. * * @package REALLY_SIMPLE_SSL */ class Rsssl_Two_Factor_Profile_Settings { use Rsssl_Two_Fa_Helper; /** * Instance of this class. * * @var Rsssl_Two_Factor_Profile_Settings */ private static $instance = null; /** * The available providers. * * @var array $available_providers An array to store the available providers. */ private $available_providers = array(); /** * The forced Two-Factor Authentication roles. * * @var array $forced_two_fa An array to store the forced Two-Factor Authentication roles. */ private array $forced_two_fa = array(); /** * Get instance of this class. * * @return Rsssl_Two_Factor_Profile_Settings */ public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } /** * Constructor for the class. * * If the user is logged in, retrieve the user object and check if two-factor authentication is turned on for the user. * If two-factor authentication is enabled, add the necessary hooks. * * @return void */ private function __construct() { if ( is_user_logged_in() ) { $user_id = get_current_user_id(); $user = get_user_by( 'ID', $user_id ); global $pagenow; $relevant_ajax_actions = [ 'change_method_to_email', 'resend_email_code_profile' ]; if ( 'profile.php' === $pagenow || ( 'user-edit.php' === $pagenow && isset( $_GET['user_id'] ) ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_REQUEST['action'] ) && in_array( $_REQUEST['action'], $relevant_ajax_actions, true ) ) ) { if ( $this->validate_two_turned_on_for_user( $user ) ) { add_action( 'admin_init', array( $this, 'add_hooks' ) ); } } } } /** * Add hooks for user profile page. * * This method adds hooks to display the Two-Factor Authentication settings on user profile pages. * * @return void */ public function add_hooks(): void { if (is_user_logged_in()) { $errors = Rsssl_Parameter_Validation::get_cached_errors(get_current_user_id()); if (!empty($errors)) { // We display the errors. foreach ($errors as $error) { add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-error', $error['message'], $error['type'] ); } } } add_action('show_user_profile', array($this, 'show_user_profile')); add_action('edit_user_profile', array($this, 'show_user_profile')); add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts')); add_action('admin_enqueue_scripts', array($this, 'enqueue_styles')); add_action('personal_options_update', array($this, 'save_user_profile')); add_action('edit_user_profile_update', array($this, 'save_user_profile')); add_action( 'wp_ajax_resend_email_code_profile', [$this, 'resend_email_code_profile_callback'] ); add_action( 'wp_ajax_change_method_to_email', [$this, 'start_email_validation_callback'] ); if (isset($_GET['profile'], $_GET['_wpnonce']) && rest_sanitize_boolean(wp_unslash($_GET['profile']))) { self::set_active_provider(get_current_user_id(), 'email'); } } /** * Resend the email code for the user. * * @return void */ public function resend_email_code_profile_callback(): void { // Check for nonce (make sure your nonce name and action match what you output to the page) if ( ! isset( $_POST['login_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['login_nonce'] ) ), 'update_user_two_fa_settings' ) ) { wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'really-simple-ssl' ) ), 403 ); } // Ensure the user is logged in. if ( ! is_user_logged_in() ) { wp_send_json_error( array( 'message' => __( 'User not logged in.', 'really-simple-ssl' ) ), 401 ); } // Get the user ID. $user_id = get_current_user_id(); $user = get_user_by( 'ID', $user_id ); Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, true); wp_send_json_success( array( 'message' => __('Verification code re-sent', 'really-simple-ssl') ), 200 ); } /** * Starts the process of email validation for a user. * */ public function start_email_validation_callback(): void { if(!is_user_logged_in()) { wp_send_json_error( array( 'message' => __( 'User not logged in.', 'really-simple-ssl' ) ), 401 ); } $user = get_user_by('id', get_current_user_id()); // Sending the email with the code. Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, true); $token = get_user_meta( $user->ID, Rsssl_Two_Factor_Email::RSSSL_TOKEN_META_KEY, true ); wp_send_json_success( array( 'message' => __('Verification code sent', 'really-simple-ssl'), 'token' => $token ), 200 ); } /** * Save the Two-Factor Authentication settings for the user. * * @param int $user_id The user ID. * * @noinspection UnusedFunctionResultInspection * @return void */ public function save_user_profile(int $user_id): void { // We check if the user owns the profile. if (!current_user_can('edit_user', $user_id)) { return; } // Handle reset action if (isset($_POST['change_2fa_config_field'])) { if ( isset($_POST['reset_two_fa_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['reset_two_fa_nonce'])), 'reset_two_fa_settings') ) { $reset_input = filter_var($_POST['change_2fa_config_field'], FILTER_VALIDATE_BOOLEAN); $this->maybe_the_user_resets_config($user_id, $reset_input); add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-reset', __('Two-Factor Authentication settings have been reset.', 'really-simple-ssl'), 'updated' ); // Redirect to avoid form resubmission wp_redirect(add_query_arg('settings-updated', 'true')); exit; } return; } if (isset($_POST['rsssl_two_fa_nonce']) && !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['rsssl_two_fa_nonce'])), 'update_user_two_fa_settings')) { return; } if (isset($_POST['change_2fa_config_field'])) { // We sanitize the input needs to be a boolean. $reset_input = filter_var($_POST['change_2fa_config_field'], FILTER_VALIDATE_BOOLEAN); $this->maybe_the_user_resets_config($user_id, $reset_input); return; } $params = new Rsssl_Parameter_Validation(); $params::validate_user_id($user_id); $user = get_user_by('ID', $user_id); $params::validate_user($user); if (!isset($_POST['two-factor-authentication'])) { // reset the user's 2fa settings. // Delete all 2fa related user meta. Rsssl_Two_Fa_Status::delete_two_fa_meta($user->ID); // Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa. update_user_meta($user->ID, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s')); // also make sure no lingering errpr messages are shown. Rsssl_Parameter_Validation::delete_cached_errors($user_id); return; } if (!isset($_POST['preferred_method'])) { return; } // now we check witch provider is selected from the $_POST. $params::validate_selected_provider($this->sanitize_method(sanitize_text_field(wp_unslash($_POST['preferred_method'])))); $selected_provider = $this->sanitize_method(sanitize_text_field(wp_unslash($_POST['preferred_method']))); // if the selected provider is not then return. if (!$selected_provider) { return; } switch ($selected_provider) { case 'totp': $current_status = Rsssl_Two_Factor_Settings::get_user_status('totp', $user_id); // if ('active' === $current_status) { // return; // } if ((empty($_POST['two-factor-totp-authcode'])) || !isset($_POST['two-factor-totp-key']) ) { add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-error', __('Two-Factor Authentication for TOTP failed. No Authentication code provided, please try again.', 'really-simple-ssl'), ); $params::cache_errors($user_id); return; } $params::validate_auth_code(absint(wp_unslash($_POST['two-factor-totp-authcode']))); $params::validate_key(sanitize_text_field(wp_unslash($_POST['two-factor-totp-key']))); $auth_code = sanitize_text_field(wp_unslash($_POST['two-factor-totp-authcode'])); $key = sanitize_text_field(wp_unslash($_POST['two-factor-totp-key'])); if (Rsssl_Two_Factor_Totp::setup_totp($user, $key, $auth_code)) { self::set_active_provider($user_id, 'totp'); // We generate the backup codes. Rsssl_Two_Factor_Backup_Codes::generate_codes( $user, array( 'cached' => true, ) ); } else { add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-error', __('The Two-Factor Authentication setup for TOTP failed. Please try again.', 'really-simple-ssl'), ); } // We cache the errors. $params::cache_errors($user_id); break; case 'email': $current_status = Rsssl_Two_Factor_Settings::get_user_status('email', $user_id); if ('active' === $current_status) { return; } $user = get_user_by('ID', $user_id); // fetch current status of the user for the email method. $status = Rsssl_Two_Factor_Settings::get_user_status('email', $user->ID); if ('active' === $status) { return; } if (Rsssl_Two_Factor_Email::get_instance()->validate_authentication($user)) { self::set_active_provider($user->ID, 'email'); } else { add_settings_error( 'two-factor-authentication', 'rsssl-two-factor-authentication-error', __('The Two-Factor Authentication setup for email failed. Please try again.', 'really-simple-ssl'), ); } break; case 'none': // We disable the Two-Factor Authentication. Rsssl_Two_Fa_Status::delete_two_fa_meta($user->ID); break; default: break; } $params::cache_errors($user_id); } /** * Sanitize the input method. * * @param string $method The input method. * * @return string The sanitized input method. Defaults to 'email' if not found in the allowed methods. */ private function sanitize_method(string $method): string { $methods = array('totp', 'email', 'passkey', 'none'); return in_array($method, $methods, true) ? sanitize_text_field($method) : 'email'; } /** * Display the user profile with Two-Factor Authentication settings. * * @param WP_User $user The user object. * * @noinspection UnusedFunctionResultInspection * @return void * @throws Exception Throws an exception if the template file is not found. */ public function show_user_profile(WP_User $user): void { // Check if the current user is viewing their own profile if ($user->ID !== get_current_user_id()) { return; } settings_errors('two-factor-authentication'); settings_errors('rsssl-two-factor-authentication-error'); $loader = Rsssl_Provider_Loader::get_loader(); $available_providers = $loader::get_enabled_providers_for_user($user); $forced = !empty(array_intersect($user->roles, $this->forced_two_fa)); $one_enabled = 'onboarding' !== Rsssl_Two_Factor_Settings::get_login_action($user->ID); $selected_provider = ''; if ($one_enabled) { $selected_provider = strtolower(Rsssl_Two_Factor_Settings::get_configured_provider($user->ID)); } $backup_codes = ''; $key = ''; $totp_url = ''; /* * Added this as a temporary fix to prevent errors when TOTP is not available. * TODO: Make a better solution to handle the case when TOTP is not available. */ if (isset($available_providers['totp'])) { $backup_codes = Rsssl_Two_Factor_Settings::get_backup_codes( $user->ID ); $key = Rsssl_Two_Factor_Totp::generate_key(); $totp_url = Rsssl_Two_Factor_Totp::generate_qr_code_url( $user, $key ); } wp_nonce_field('update_user_two_fa_settings', 'rsssl_two_fa_nonce'); // Pass user_id instead of user object to prevent object corruption during template rendering $user_id = $user->ID; $data = array( 'key' => $key, 'totp_url' => $totp_url, 'backup_codes' => $backup_codes, 'selected_provider' => $selected_provider, 'one_enabled' => $one_enabled, 'forced' => $forced, 'available_providers' => $available_providers, 'user_id' => $user_id, 'login_nonce' => wp_create_nonce('rsssl_login_nonce'), ); $data = self::removeCircularReferences($data); $data_js = 'rsssl_profile.totp_data = ' . json_encode($data, JSON_THROW_ON_ERROR) . ';'; $passkeys_enabled = rsssl_get_option('enable_passkey_login' ); wp_add_inline_script('rsssl-profile-settings', $data_js); // We load the needed template for the Two-Factor Authentication settings. rsssl_load_template( 'profile-settings.php', compact( 'user_id', 'available_providers', 'forced', 'one_enabled', 'selected_provider', 'backup_codes', 'totp_url', 'key', 'passkeys_enabled' ), rsssl_path . 'assets/templates/two_fa/' ); } /** * Validates if the Two-Factor Authentication is turned on for the user. * * @param WP_User $user The user object. * * @return bool Returns true if Two-Factor Authentication is turned on for the user, false otherwise. */ private function validate_two_turned_on_for_user(WP_User $user): bool { // Get the setting for the system to check if it is turned on. $enabled_two_fa = rsssl_get_option('login_protection_enabled'); $providers = Rsssl_Provider_Loader::get_loader()::get_user_enabled_providers($user); $option = rsssl_get_option('two_fa_forced_roles'); $this->forced_two_fa = $option !== false ? $option : array(); return $enabled_two_fa && !empty($providers); } /** * Enqueues the RSSSL profile settings script. * * @return void */ public function enqueue_scripts(): void { $path = trailingslashit(rsssl_url) . 'assets/features/two-fa/assets.min.js'; $file_path = trailingslashit(rsssl_path) . 'assets/features/two-fa/assets.min.js'; $backup_codes = Rsssl_Two_Factor_Settings::get_backup_codes(get_current_user_id()); $user = get_user_by('ID', get_current_user_id()); // We check if the backup codes are available. wp_register_script('rsssl-profile-settings', $path, array(), filemtime($file_path), true); wp_enqueue_script('rsssl-profile-settings'); wp_localize_script('rsssl-profile-settings', 'rsssl_profile', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'backup_codes' => $backup_codes, 'root' => esc_url_raw(rest_url(Rsssl_Two_Factor::REST_NAMESPACE)), 'user_id' => get_current_user_id(), 'origin' => 'profile', 'redirect_to' => 'rsssl_no_redirect', //added this for comparison in the json output. 'login_nonce' => Rsssl_Two_Fa_Authentication::create_login_nonce(get_current_user_id())['rsssl_key'], 'user_name' => $user->display_name, 'display_name' => $user->user_nicename . ' (' . $user->user_email . ')', 'translatables' => apply_filters('rsssl_two_factor_translatables', []), )); } /** * Enqueues the RSSSL profile settings stylesheet. * * @return void */ public function enqueue_styles(): void { $path = trailingslashit(rsssl_url) . 'assets/features/two-fa/styles.css'; $file_path = trailingslashit(rsssl_path) . 'assets/features/two-fa/styles.css'; wp_enqueue_style('rsssl-profile-style', $path, array(), filemtime($file_path)); } /** * Checks if the user resets the configuration and actually reset everything. * * @param int $user_id The ID of the user. * @param $reset_input * * @return bool */ private function maybe_the_user_resets_config(int $user_id, $reset_input): bool { // If the reset is true, we do the reset. if ($reset_input && $user_id) { // We reset the user's Two-Factor Authentication settings. Rsssl_Two_Fa_Status::delete_two_fa_meta($user_id); } return $reset_input; } /** * Remove circular references from the data. * * @param $data * @param array $seen * @return mixed|null */ public static function removeCircularReferences(&$data, array &$seen = []) { if (is_array($data) || is_object($data) ) { if (in_array($data, $seen, true)) { return null; // Circular reference detected, return null or handle appropriately } $seen[] = $data; foreach ($data as &$value) { $value = self::removeCircularReferences($value, $seen); } } return $data; } } } wordpress/two-fa/class-rsssl-two-factor-compat.php 0000777 00000002623 15251751331 0016334 0 ustar 00 <?php /** * A compatibility layer for some of the most popular plugins. * * @package Two_Factor */ namespace RSSSL\Security\WordPress\Two_Fa; use Jetpack; /** * A compatibility layer for some of the most popular plugins. * * Should be used with care because ideally we wouldn't need * any integration specific code for this plugin. Everything should * be handled through clever use of hooks and best practices. */ class Rsssl_Two_Factor_Compat { /** * Initialize all the custom hooks as necessary. * * @return void */ public function init() { /** * Jetpack * * @see https://wordpress.org/plugins/jetpack/ */ add_filter( 'rsssl_two_factor_rememberme', array( $this, 'jetpack_rememberme' ) ); } /** * Jetpack single sign-on wants long-lived sessions for users. * * @param boolean $rememberme Current state of the "remember me" toggle. * * @return boolean */ public function jetpack_rememberme( $rememberme ) { $action = filter_input( INPUT_GET, 'action', FILTER_CALLBACK, array( 'options' => 'sanitize_key' ) ); if ( 'jetpack-sso' === $action && $this->jetpack_is_sso_active() ) { return true; } return $rememberme; } /** * Helper to detect the presence of the active SSO module. * * @return boolean */ public function jetpack_is_sso_active() { return ( method_exists( '\Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'sso' ) ); } } wordpress/two-fa/class-rsssl-two-fa-authentication.php 0000777 00000006402 15251751331 0017177 0 ustar 00 <?php /** * Two-Factor Authentication. * * @package REALLY_SIMPLE_SSL * * @since 0.1-dev */ namespace RSSSL\Security\WordPress\Two_Fa; use Exception; /** * Class Rsssl_Two_Fa_Authentication * * Represents the two-factor authentication functionality. */ class Rsssl_Two_Fa_Authentication { /** * The user meta nonce key. * * @type string */ public const RSSSL_USER_META_NONCE_KEY = '_rsssl_two_factor_nonce'; /** * Verify a login nonce for a user. * * @param int $user_id The ID of the user. * @param string $nonce The login nonce to verify. * * @return bool True if the nonce is valid and has not expired, false otherwise. */ public static function verify_login_nonce( int $user_id, string $nonce ): bool { $login_nonce = get_user_meta( $user_id, self::RSSSL_USER_META_NONCE_KEY, true ); if ( ! $login_nonce || empty( $login_nonce['rsssl_key'] ) || empty( $login_nonce['rsssl_expiration'] ) ) { return false; } $unverified_nonce = array( 'rsssl_user_id' => $user_id, 'rsssl_expiration' => $login_nonce['rsssl_expiration'], 'rsssl_key' => $nonce, ); $unverified_hash = self::hash_login_nonce( $unverified_nonce ); $hashes_match = $unverified_hash && hash_equals( $login_nonce['rsssl_key'], $unverified_hash ); if ( $hashes_match && time() < $login_nonce['rsssl_expiration'] ) { return true; } // Require a fresh nonce if verification fails. self::delete_login_nonce( $user_id ); return false; } /** * Create a login nonce for a user. * * @param int $user_id The ID of the user. * * @return array|false The login nonce array if successfully created and stored, false otherwise. */ public static function create_login_nonce( int $user_id ) { $login_nonce = array( 'rsssl_user_id' => $user_id, 'rsssl_expiration' => time() + ( 15 * MINUTE_IN_SECONDS ), ); try { $login_nonce['rsssl_key'] = bin2hex( random_bytes( 32 ) ); } catch ( Exception $ex ) { $login_nonce['rsssl_key'] = wp_hash( $user_id . wp_rand() . microtime(), 'nonce' ); } // Store the nonce hashed to avoid leaking it via database access. $hashed_key = self::hash_login_nonce( $login_nonce ); if ( $hashed_key ) { $login_nonce_stored = array( 'rsssl_expiration' => $login_nonce['rsssl_expiration'], 'rsssl_key' => $hashed_key, ); if ( update_user_meta( $user_id, self::RSSSL_USER_META_NONCE_KEY, $login_nonce_stored ) ) { return $login_nonce; } } return false; } /** * Delete the login nonce. * * @param int $user_id User ID. * * @return bool * @since 0.1-dev */ public static function delete_login_nonce( int $user_id ): bool { return delete_user_meta( $user_id, self::RSSSL_USER_META_NONCE_KEY ); } /** * Get the hash of a nonce for storage and comparison. * * @param array $nonce Nonce array to be hashed. ⚠️ This must contain user ID and expiration, * to guarantee the nonce only works for the intended user during the * intended time window. * * @return string|false */ protected static function hash_login_nonce( array $nonce ) { $message = wp_json_encode( $nonce ); if ( ! $message ) { return false; } return wp_hash( $message, 'nonce' ); } } wordpress/two-fa/class-rsssl-two-factor-settings.php 0000777 00000063306 15251751331 0016716 0 ustar 00 <?php /** * Holds the request parameters for a specific action. * This class holds the request parameters for a specific action. * It is used to store the parameters and pass them to the functions. * * @package REALLY_SIMPLE_SSL */ namespace RSSSL\Security\WordPress\Two_Fa; use RSSSL\Pro\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Passkey; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Provider_Loader; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Email; use RSSSL\Pro\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Totp; use WP_User; /** * Class Rsssl_Two_Factor_Settings * * This class handles the settings for the Two-Factor Authentication plugin. */ class Rsssl_Two_Factor_Settings { /** * The class instance. * * @var Rsssl_Two_Factor_Settings */ private static $instance; /** * The forced roles for 2FA. * * @var array $forced_roles */ public static $forced_roles; /** * The enabled roles for TOTP. * * @var $enabled_roles_totp */ public static $enabled_roles_totp; /** * The forced roles for TOTP dynamically generated by logic. * * @var $forced_roles_totp */ public static $forced_roles_totp; // @codingStandardsIgnoreLine It is dynamically generated by logic. /** * The enabled roles for Email dynamically generated by logic. * * @var $enabled_roles_totp */ public static $forced_roles_email; // @codingStandardsIgnoreLine It is dynamically generated by logic. /** * The enabled roles for Email. * * @var array $enabled_roles_email */ public static $enabled_roles_email; /** * The enabled roles for Passkey, hint they all are. * * @var array $enabled_roles_passkey */ public static $enabled_roles_passkey; /** * The forced roles for Passkey. * * @var array $forced_roles_passkey */ public static $forced_roles_passkey; /** * If the previous roles variables are loaded or not. * * @var bool $roles_loaded */ private static $roles_loaded = false; /** * The user meta enabled providers key. * * @type string */ const RSSSL_ENABLED_PROVIDERS_USER_META_KEY = 'rsssl_two_fa_providers'; /** * Class constructor. * * Checks if the class instance has already been initialized. If so, returns * immediately. Otherwise, assigns the class instance to the static variable * "self::$instance". */ public function __construct() { if ( isset( self::$instance ) ) { return; } self::$instance = $this; } /** * Get user roles for a user, cross multisite. * * @param int $user_id //the user id to get the roles for. * * @return array */ public static function get_user_roles( int $user_id ): array { if ( is_multisite() ) { $strict_roles = self::get_strictest_role_across_sites($user_id, ['totp', 'email']); if ( is_string( $strict_roles ) && '' !== $strict_roles ) { return array( $strict_roles ); } if ( is_array( $strict_roles ) ) { return array_values( $strict_roles ); } return array(); } $user = get_userdata( $user_id ); $roles = $user->roles; if ( ! is_array( $roles ) ) { $roles = array(); } return $roles; } /** * Generate a one-time login URL for a user. * * @param int $user_id //the user ID. * @param bool $disable_two_fa //whether to disable two-factor authentication. * * @return string //the generated URL. */ public static function rsssl_one_time_login_url( int $user_id, bool $disable_two_fa = false, $profile = false ): string { $token = bin2hex( openssl_random_pseudo_bytes( 16 ) ); // 16 bytes * 8 bits/byte = 128 bits. set_transient( 'skip_two_fa_token_' . $user_id, $token, 2 * MINUTE_IN_SECONDS ); $obfuscated_user_id = self::obfuscate_user_id( $user_id ); $nonce = wp_create_nonce( 'one_time_login_' . $user_id ); if(!$profile) { $args = array( 'rsssl_one_time_login' => $obfuscated_user_id, 'token' => $token, '_wpnonce' => $nonce, ); } else { $args = array( '_wpnonce' => $nonce, 'profile' => $profile, ); } if (function_exists('rsssl_get_option') && rsssl_get_option('change_login_url_enabled') !== false && !empty(rsssl_get_option('change_login_url'))) { $login_url = trailingslashit(site_url()) . rsssl_get_option('change_login_url'); } else { $login_url = wp_login_url(); } if ( $disable_two_fa ) { $args['rsssl_two_fa_disable'] = true; } // Return the URL with the added query arguments. return add_query_arg( $args, $profile? get_edit_profile_url( $user_id ):$login_url ); } /** * Get the {method}_role_status. The role with the most weighing status will be returned. empty, optional or forded. Where forced is the most weighing. * * @param string $method //the method to check. * @param int $user_id //the user id to get the roles for. * * @return string */ public static function get_role_status( string $method, int $user_id ): string { $roles = array(); if ( is_multisite() ) { $strict_roles = self::get_strictest_role_across_sites( $user_id, array( $method ) ); if ( is_string( $strict_roles ) && '' !== $strict_roles ) { $roles = array( $strict_roles ); } if ( is_array( $strict_roles ) ) { $roles = array_values( $strict_roles ); } } else { $roles = self::get_user_roles( $user_id ); } // Early return for non-passkey methods with no roles. if ( empty( $roles ) && 'passkey' !== $method ) { return 'empty'; } $provider = 'email' === $method ? '_email' : '_' . self::sanitize_method( $method ); // Check if the method is enabled. $enabled = ($method === 'passkey') ? array_keys(wp_roles()->roles) : rsssl_get_option("two_fa_enabled_roles$provider"); $forced = false; if ( ! $enabled ) { $return = 'empty'; } // if the role is forced, return forced. if ( self::contains_role_of_type( $method, $roles, 'forced' ) ) { $return = 'forced'; $forced = true; } // if the method = 'passkeys' and the role is forced, return forced. if ('passkey' === $method && self::contains_role_of_type($method, $roles, 'forced')) { $return = 'forced'; $forced = true; } //if the method = 'passkey' and the role is enabled, return optional. if ('passkey' === $method && $enabled) { $return = 'optional'; } // if the role is enabled, return optional. if ( self::contains_role_of_type( $method, $roles, 'enabled' ) && ! $forced ) { $return = 'optional'; } if ( empty( $return ) ) { $return = 'empty'; } return $return; } public static function get_login_action(?int $user_id = null): string { if ( null === $user_id ) { $user_id = get_current_user_id(); } $user = get_userdata( $user_id ); $loader = Rsssl_Provider_Loader::get_loader(); $available_providers = $loader::available_providers(); $ProvidersWithStatus = self::addStatusToProviders($available_providers, $user); // first we filter if the array gas an active status. $active = array_filter( $ProvidersWithStatus, function ( $provider ) { return 'active' === $provider['status']; } ); // if the array is not empty, we return the first key. if ( ! empty( $active ) ) { $active = array_keys( $active ); return reset( $active ); } foreach ($available_providers as $method => $provider_class ) { if ( $provider_class::is_enabled( $user ) ) { $user_status = self::get_user_status( $method, $user_id ); $role_status = self::get_role_status( $method, $user_id ); if ( 'active' === $user_status && ( 'forced' === $role_status || 'optional' === $role_status ) ) { return $method; // Return the method directly if active and role status matches. } if ( 'open' === $user_status && ( 'forced' === $role_status || 'optional' === $role_status ) ) { $grace_period = self::is_user_in_grace_period( $user ); if ( $grace_period > 0 && 'forced' === $role_status ) { return 'onboarding'; } if ( 'optional' === $role_status ) { return 'onboarding'; } return 'expired'; } // If role is forced and status isn't disabled, return onboarding. if ( 'forced' === $role_status && 'disabled' !== $user_status ) { return 'onboarding'; } } } return self::get_email_method_action( $user_id ); // Fallback to email or other default behavior. } public static function addStatusToProviders( array $providers, $user ): array { $filtered_providers = []; foreach ( $providers as $method => $provider_class ) { if ( $provider_class::is_enabled( $user ) ) { $user_status = self::get_user_status( $method, $user->ID ); $role_status = self::get_role_status( $method, $user->ID ); $filtered_providers[ $method ] = array( 'status' => $user_status, 'role' => $role_status, 'class' => $provider_class, ); } else { $filtered_providers[ $method ] = array( 'status' => 'disabled', 'role' => 'disabled', 'class' => $provider_class, ); } } return $filtered_providers; } /** * Get required action for the email 2fa method. * * @param int $user_id //the user id to get the roles for. * * @return string //email, onboarding or login */ public static function get_email_method_action( int $user_id ): string { $email = Rsssl_Two_Factor_Email::get_instance(); $grace_period = self::is_user_in_grace_period( get_userdata( $user_id ) ); $return = 'login'; if ( $email::is_enabled( get_userdata( $user_id ) ) ) { $user_status = self::get_user_status( 'email', $user_id ); $role_status = self::get_role_status( 'email', $user_id ); if ( 'active' === $user_status ) { // Also check the role status, in case the admin has disabled this for this role. if ( 'forced' === $role_status || 'optional' === $role_status ) { $return = 'email'; } } if ( 'open' === $user_status ) { // if the role status is forced or optional, we show onboarding. if ( 'forced' === $role_status || 'optional' === $role_status ) { // The role is forced. So check if the grace period is over. if ( $grace_period > 0 && 'forced' === $role_status ) { return 'onboarding'; } if ('optional' === $role_status) { return 'onboarding'; } return 'expired'; } } } // if we're here, the email method is not enabled, so we show login. return $return; } /** * Validate if the role status and user status are valid. * * @param string $role_status // The role status to check. * @param string $user_status // The user status to check. * * @return bool // Returns true if the role status and user status are valid, otherwise false. */ public static function is_role_and_user_status_valid( string $role_status, string $user_status ): bool { return ( 'forced' === $role_status || 'optional' === $role_status ) && ( 'active' === $user_status || 'open' === $user_status ); } /** * Get the status for a user, based on the method. * * @param string $method //the method to check. * @param int $user_id //the user id to get the roles for. * * @return string //open, active or disabled */ public static function get_user_status( string $method, int $user_id ): string { $method = 'email' === $method ? '_email' : '_' . self::sanitize_method( $method ); // first check if a user meta rsssl_two_fa_status is set. $status = get_user_meta( $user_id, "rsssl_two_fa_status$method", true ); return self::sanitize_status( $status ); } /** * Get the roles for a user, based on the method and type. * * @param string $method //the method to check. * @param string $type //the type to check. * * @return array */ private static function get_dynamic_roles_variable( string $method, string $type ): array { // store these roles, as this function can be used in large loops. if ( ! self::$roles_loaded ) { // if the option is a boolean we convert it to an array. self::$enabled_roles_totp = rsssl_get_option( 'two_fa_enabled_roles_totp', [] ); self::$enabled_roles_email = rsssl_get_option( 'two_fa_enabled_roles_email', [] ); // Passkey is always enabled. So all roles are enabled. self::$enabled_roles_passkey = array_values(wp_roles()->get_names()); self::$forced_roles = rsssl_get_option( 'two_fa_forced_roles', [] ); self::$roles_loaded = true; } $method = 'email' === $method ? '_email' : '_' . self::sanitize_method( $method ); $type = 'enabled' === $type ? 'enabled' : 'forced'; $name = $type . '_roles' . $method; $roles_to_check = 'enabled_roles' . $method; // if the type is forced, use the forced roles. if ( 'forced' === $type ) { // Intersect the roles with the enabled roles. self::$$name = array_intersect( self::$forced_roles, self::$$roles_to_check ); if ( property_exists( self::class, $name ) ) { $roles = self::$$name; if ( ! is_array( $roles ) ) { $roles = array(); } return $roles; } } // if the type is enabled, use the enabled roles. if ( 'enabled' === $type ) { self::$$name = array_merge( self::$$roles_to_check ); if ( property_exists( self::class, $name ) ) { $roles = self::$$name; if ( ! is_array( $roles ) ) { $roles = array(); } return $roles; } } return array(); } /** * Check if the array of roles contains a role of type $type, forced or optional. * * @param string $method //the method to check. * @param array $roles //the roles to check. * @param string $type //the type to check. * * @return bool */ public static function contains_role_of_type( string $method, array $roles, string $type ): bool { $roles_to_check = self::get_dynamic_roles_variable( $method, $type ); foreach ( $roles as $role ) { if ( in_array( $role, $roles_to_check, true ) ) { return true; } } return false; } /** * Check if a role is of a certain type, optional or forced * * @param string $method //the method to check. * @param string $role //the role to check. * @param string $type //the type to check. * * @return bool */ public static function role_is_of_type( string $method, string $role, string $type ): bool { return self::contains_role_of_type( $method, array( $role ), $type ); } /** * Get the user meta enabled providers key. * * @param string $status //the status to filter by. * * @return string //the user meta key. */ protected static function sanitize_status( string $status ): string { return in_array( $status, array( 'open', 'active', 'disabled' ), true ) ? $status : 'open'; } /** * Get the user meta enabled providers key. * * @param string $method //the method to sanitize. * * @return string */ public static function sanitize_method( string $method ): string { return in_array( $method, array( 'email', 'totp', 'passkey' ), true ) ? $method : 'email'; } /** * Check if a user is forced to use 2FA based on their roles. * * @param int $user_id // the ID of the user to check. * * @return bool // true if the user is forced to use 2FA, false otherwise. */ public static function is_user_forced_to_use_2fa( int $user_id ): bool { $roles = self::get_user_roles( $user_id ); $forced_roles = rsssl_get_option( 'two_fa_forced_roles', [] ); foreach ( $roles as $role ) { if ( in_array( $role, $forced_roles, true ) ) { return true; } } return false; } /** * Check if a user is in the grace period for two-factor authentication. * * @param WP_User $user The user to check. * * @return int|false The number of days remaining in the grace period, or false if the user is not in the grace period. */ public static function is_user_in_grace_period( WP_User $user ) { $grace_period = rsssl_get_option( 'two_fa_grace_period'); // if the grace period is not set, return false. if ( ! self::is_user_forced_to_use_2fa( $user->ID ) ) { return false; } $last_login = get_user_meta( $user->ID, 'rsssl_two_fa_last_login', true ); if ( $last_login ) { $last_login = strtotime( $last_login ); $now = time(); $diff = $now - $last_login; $days = floor( $diff / ( 60 * 60 * 24 ) ); if ( $days < $grace_period ) { $end_date = gmdate( 'Y-m-d', $last_login ); // We add the grace period to the last login date. $end_date = date( 'Y-m-d', strtotime( $end_date . ' + ' . $grace_period . ' days' ) ); $today = gmdate('Y-m-d', $now); // If the end date is today, return 1. if ($end_date === $today) { return 1; } return $grace_period - $days; } // it is now equal or greater, so return false. return false; } // if the last login is not set, return the grace period. but also set the user meta. update_user_meta( $user->ID, 'rsssl_two_fa_last_login', gmdate( 'Y-m-d H:i:s' ) ); return $grace_period; } /** * Get the enabled roles for a user. * * @param int $user_id // The ID of the user. * * @return array // The array of enabled roles for the user. */ public static function get_enabled_roles( int $user_id ): array { $roles = self::get_user_roles( $user_id ); if(defined('rsssl_pro') && rsssl_pro ) { $totp = rsssl_get_option( 'two_fa_enabled_roles_totp', [] ); } else { $totp = []; } $email = rsssl_get_option( 'two_fa_enabled_roles_email', [] ); $passkey = array_keys(wp_roles()->roles); $enabled_roles = array_merge( $totp, $email, $passkey ); return array_intersect( $roles, $enabled_roles ); } /** * Get the enabled roles for a user. * This function is used to get the roles that are enabled for a user. * * @param int $user_id //the user ID to obfuscate. * * @return string */ public static function obfuscate_user_id( int $user_id ): string { // Convert the user ID to a string with some noise. $obfuscated = 'user-' . $user_id . '-id'; // Encode the string using base64. return base64_encode( $obfuscated ); } /** * Deobfuscate the user ID for use in URL. * * @param string $data //the data to deobfuscate. * * @return string|null */ public static function deobfuscate_user_id( string $data ): ?string { // Decode from base64. $decoded = base64_decode( $data ); // Remove the noise to get the user ID. if ( preg_match( '/user-(\d+)-id/', $decoded, $matches ) ) { return $matches[1]; } return null; } /** * Based on the roles enabled return the method for the current user. * If both methods are enabled, return the string not set. * If only one method is enabled, return that method as a string. * If no method is enabled, return the string None. * * @param int $user_id //the user ID to get the roles for. * * @return string */ public static function get_enabled_method( int $user_id ): string { $user_id = absint( $user_id ); // make sure an integer and not a float, negative value. $enabled_roles = self::get_enabled_roles( $user_id ) ?? array(); $enabled_totp = rsssl_get_option( 'two_fa_enabled_roles_totp', [] ); $enabled_email = rsssl_get_option( 'two_fa_enabled_roles_email', [] ); $totp = array_intersect( $enabled_roles, $enabled_totp ); $email = array_intersect( $enabled_roles, $enabled_email ); if ( ! empty( $totp ) && ! empty( $email ) ) { $enabled_method = __( 'not set', 'really-simple-ssl' ); } if ( ! empty( $totp ) ) { $enabled_method = __( 'Authenticator App', 'really-simple-ssl' ); } if ( ! empty( $email ) ) { $enabled_method = __( 'Email', 'really-simple-ssl' ); } if ( ! isset( $enabled_method ) ) { $enabled_method = __( 'None', 'really-simple-ssl' ); } return $enabled_method; } /** * Get the configured provider for a user based on their ID. * * @param int $user_id The ID of the user. * * @return string The configured provider. */ public static function get_configured_provider( int $user_id ): string { // With 2 providers, TOTP and Email we check both options and get the one that is not disabled. $totp_meta = get_user_meta( $user_id, 'rsssl_two_fa_status_totp', true ); $email_meta = get_user_meta( $user_id, 'rsssl_two_fa_status', true ); $passkey_meta = get_user_meta( $user_id, 'rsssl_two_fa_status_passkey', true ); $provider = __( 'None', 'really-simple-ssl' ); // if the status is active, return the method. if ( 'active' === $totp_meta ) { $provider = Rsssl_Two_Factor_Totp::NAME; } if ( 'active' === $email_meta ) { $provider = Rsssl_Two_Factor_Email::NAME; } if ('active' === $passkey_meta) { $provider = Rsssl_Two_Factor_Passkey::NAME; } return $provider; } /** * Get the backup codes for a user. * * @param int $user_id // The user ID. * * @return array // An array of backup codes. */ public static function get_backup_codes( int $user_id ): array { $codes = get_transient( 'rsssl_two_factor_backup_codes_' . $user_id ); if ( ! is_array( $codes ) ) { $codes = array(); } return $codes; } /** * Check if the last login date for a user is today. * * @param WP_User $user //the user. * * @return bool //true if last login date is today, false otherwise. */ public static function is_today( WP_User $user ): bool { return (1 === (int) self::is_user_in_grace_period( $user )); } /** * Ensure that the default roles are first in the array * * * @return array */ protected static function sort_roles_by_default_first( array $roles ): array { $default_roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' ); $sorted_roles = array(); foreach ( $default_roles as $default_role ) { if ( in_array( $default_role, $roles, true ) ) { $sorted_roles[] = $default_role; } } foreach ( $roles as $role ) { if ( ! in_array( $role, $sorted_roles, true ) ) { $sorted_roles[] = $role; } } return $sorted_roles; } /** * Get the strictest role across all sites for a given user * * @param int $user_id //the ID of the user. * * @return array|null //returns the strictest role or null if no roles found. */ public static function get_strictest_role_across_sites(int $user_id, $methods ): ?array { $sites = get_sites(); $all_roles = []; foreach ($sites as $site) { switch_to_blog($site->blog_id); $user = get_userdata($user_id); if ($user && is_array($user->roles)) { foreach($user->roles as $role){ $all_roles[] = $role; } } restore_current_blog(); } $all_roles = array_unique($all_roles); return self::get_strictest_role($methods, $all_roles); } /** * Get the strictest role from a list of roles * * @param array $roles // The list of roles * @return array // The strictest role */ protected static function get_strictest_role(array $methods, array $roles): array { $result = []; if (is_multisite()) { $roles = self::sort_roles_by_default_first($roles); $forced_roles = rsssl_get_option('two_fa_forced_roles', []); // if there are forced roles, prioritize them by removing all other roles if (!empty($forced_roles) && array_intersect($roles, $forced_roles)) { $roles = array_intersect($roles, $forced_roles); } } foreach ($methods as $method) { // First, prioritize forced roles using the default-first sorting method if (self::contains_role_of_type($method, $roles, 'forced')) { foreach ($roles as $role) { if (self::role_is_of_type($method, $role, 'forced')) { // If forced role is found, assign it to the method and continue to the next method $result[$method] = $role; continue 2; } } } // If no forced role, check for optional roles if (self::contains_role_of_type($method, $roles, 'enabled')) { foreach ($roles as $role) { if (self::role_is_of_type($method, $role, 'enabled')) { // If optional role is found, assign it to the method and continue to the next method $result[$method] = $role; continue 2; } } } // If no role was found, assign an empty string $result[$method] = ''; } //remove empty values return array_values(array_unique(array_filter($result))); } /** * Get the user status per method * * @param int $user_id // The ID of the user. * * @return array // The user status per method */ public static function get_user_status_per_method(int $user_id): array { $methods = self::get_available_methods(); $result = []; foreach ($methods as $method) { $result[$method] = self::get_user_status($method, $user_id); } return $result; } private static function get_available_methods(): array { if(defined('rsssl_pro') && !rsssl_pro ) { return ['totp', 'email']; } return ['email']; } } new Rsssl_Two_Factor_Settings(); wordpress/two-fa/class-rsssl-two-factor-on-board-api.php 0000777 00000003106 15251751331 0017316 0 ustar 00 <?php /** * Handles the API routes for the two-factor authentication onboarding process. * This class is responsible for handling the API routes for the two-factor authentication onboarding process. * It registers the routes and handles the requests. * * @package REALLY_SIMPLE_SSL * @subpackage Security\WordPress\Two_Fa */ namespace RSSSL\Security\WordPress\Two_Fa; use RSSSL\Security\WordPress\Two_Fa\Controllers\Rsssl_Base_Controller; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Provider_Loader; use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Provider_Interface; /** * Registers API routes for the application. * This class is responsible for registering the API routes for the two-factor authentication onboarding process. * It registers the routes and handles the requests. * * @package REALLY_SIMPLE_SSL * @subpackage Security\WordPress\Two_Fa */ class Rsssl_Two_Factor_On_Board_Api { /** * The namespace for the API routes. * * @package really-simple-security/v1/two_fa */ public const NAMESPACE = 'really-simple-security/v1/two-fa/v2'; /** * Initializes the object and registers API routes. * * @return void */ public function __construct() { // get the correct loader $loader = Rsssl_Provider_Loader::get_loader(); new Rsssl_Base_Controller('really-simple-security', 'v1', 'v2'); foreach ($loader::available_providers() as $provider ) { /** @var Rsssl_Two_Factor_Provider_Interface $provider */ $provider::start_controller('really-simple-security', 'v1', 'v2'); } } } wordpress/two-fa/providers/interface-rsssl-two-factor-provider-interface.php 0000777 00000001704 15251751331 0023510 0 ustar 00 <?php /** * Holds the request parameters for a specific action. * * @package REALLY_SIMPLE_SSL */ namespace RSSSL\Security\WordPress\Two_Fa\Providers; use WP_User; /** * Check if a user is forced. * * @param WP_User $user The user to check. * * @return bool True if the user is forced, false otherwise. */ interface Rsssl_Two_Factor_Provider_Interface { /** * Check if a user is forced. * * @param WP_User $user The user to check. * * @return bool True if the user is forced, false otherwise. */ public static function is_forced( WP_User $user ): bool; /** * Check if a method is enabled within the roles of the user. * * @param WP_User $user The user to check. * * @return bool True if the user is enabled, false otherwise. */ public static function is_enabled( WP_User $user ): bool; public static function is_optional( WP_User $user ): bool; public static function is_configured( WP_User $user ): bool; } wordpress/two-fa/providers/class-rsssl-two-factor-provider.php 0000777 00000007523 15251751331 0020724 0 ustar 00 <?php /** * Abstract class for creating two factor authentication providers. * * @package Two_Factor */ namespace RSSSL\Security\WordPress\Two_Fa\Providers; use WP_User; /** * Abstract class for creating two-factor authentication providers. * * @since 7.0.6 * * @package Two_Factor */ abstract class Rsssl_Two_Factor_Provider { /** * The instance of the provider. * * @var Rsssl_Two_Factor_Provider */ public $instance; /** * Class constructor. * * @since 0.1-dev */ protected function __construct() { $this->instance = $this; } /** * Returns the name of the provider. * * @since 0.1-dev * * @return string */ abstract public function get_label(); /** * Prints the name of the provider. * * @since 0.1-dev */ public function print_label() { echo esc_html( $this->get_label() ); } /** * Prints the form that prompts the user to authenticate. * * @param WP_User $user WP_User object of the logged-in user. * * @since 0.1-dev */ abstract public function authentication_page( WP_User $user ); /** * Allow providers to do extra processing before the authentication. * Return `true` to prevent the authentication and render the * authentication page. * * @param WP_User $user WP_User object of the logged-in user. * @return boolean */ public function pre_process_authentication( $user ) { return false; } /** * Validates the users input token. * * @param WP_User $user WP_User object of the logged-in user. * @return boolean *@since 0.1-dev * */ abstract public function validate_authentication( WP_User $user ): bool; /** * Whether this Two Factor provider is configured and available for the user specified. * * @param WP_User $user WP_User object of the logged-in user. * @return boolean */ abstract public function is_available_for_user(WP_User $user ): bool; /** * Start the controller needed for onboarding and profile management. * @return mixed */ abstract protected static function start_controller( string $namespace, string $version, string $featureVersion ):void; /** * Generate a random eight-digit string to send out as an auth code. * * @param int $length The code length. * @param string|array $chars Valid auth code characters. * @return string *@since 0.1-dev * */ public static function get_code(int $length = 8, $chars = '1234567890' ): string { $code = ''; if ( is_array( $chars ) ) { $chars = implode( '', $chars ); } for ( $i = 0; $i < $length; $i++ ) { $code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 ); } return $code; } /** * Sanitizes a numeric code to be used as an auth code. * * @param string $field The _REQUEST field to check for the code. * @param int $length The valid expected length of the field. * * @return false|string Auth code on success, false if the field is not set or not expected length. */ public static function sanitize_code_from_request( string $field, int $length = 0 ) { if ( empty( $_REQUEST[ $field ] ) ) { return false; } $code = wp_unslash( $_REQUEST[ $field ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, handled by the core method already. $code = preg_replace( '/\s+/', '', $code ); // Maybe validate the length. if ( $length && strlen( $code ) !== $length ) { return false; } return (string) $code; } /** * Set user status. * * This function updates the 'rsssl_two_fa_status' user meta key with the provided status. * * @param int $user_id The user ID. * @param string $status The user status. * * @return void * @since 1.0.0 */ public static function set_user_status( int $user_id, string $status ): void { update_user_meta( $user_id, 'rsssl_two_fa_status', $status ); } abstract public static function reset_meta_data (int $user_id): void; } wordpress/two-fa/providers/class-rsssl-provider-loader.php 0000777 00000006123 15251751331 0020100 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Providers; use RSSSL\Pro\Security\WordPress\Two_Fa\Providers\Rsssl_Provider_Loader_Pro; use WP_User; abstract class Rsssl_Provider_Loader { public const TWO_FA_PROVIDERS = [ 'totp', 'email', 'passkey' ]; /** * Retrieves the list of available two-factor authentication providers. * * @return array The array of available provider class names indexed by method name. */ public static function available_providers(): array { $providers = static::get_providers(); return apply_filters( 'rsssl_two_factor_providers', $providers ); } public static function get_providers(): array { $providers = []; $directory = __DIR__ ; foreach ( glob( $directory . '/*.php', GLOB_NOSORT ) as $file) { $base_name = str_replace('class-', '', basename($file, '.php')); $class_name = 'RSSSL\\Security\\WordPress\\Two_Fa\\Providers\\' . str_replace(' ', '_', ucwords(str_replace('-', ' ', $base_name))); if (class_exists($class_name) && is_subclass_of($class_name, Rsssl_Two_Factor_Provider_Interface::class)) { preg_match('/Rsssl_Two_Factor_(.+)/', $class_name, $matches); $method_name = strtolower($matches[1]); $providers[$method_name] = $class_name; } } return $providers; } /** * Fetches the enabled providers for the user. * * @return array */ public static function get_enabled_providers_for_user( WP_User $user ): array { $enabled_providers = []; foreach ( static::available_providers() as $method => $class ) { if ( $class::is_enabled( $user ) ) { $enabled_providers[$method] = $class::get_instance( $user ); } } return $enabled_providers; } /** * Get the configured providers for the user. * * * @return array */ public static function get_configured_providers_for_user( WP_User $user ): array { $configured_providers = []; foreach ( static::get_enabled_providers_for_user( $user ) as $method => $provider ) { if ( $provider::is_configured( $user ) ) { $configured_providers[$method] = $provider::get_instance( $user ); } } return $configured_providers; } /** * Checks is the pro version is active. * @return bool */ public static function is_pro_active(): bool { return defined( 'rsssl_pro' ); } /** * Get the enabled providers for the user. * * @return array */ public static function get_user_enabled_providers( WP_User $user ): array { $enabled_providers = []; foreach ( self::available_providers() as $method => $provider ) { if ( $provider::is_enabled( $user ) ) { $enabled_providers[$method] = $provider::get_instance( $user ); } } return $enabled_providers; } /** * Loads the correct provider loader based on the active plugin. * * @return Rsssl_Provider_Loader_Pro|Rsssl_Provider_Loader_Free */ public static function get_loader() { return Rsssl_Provider_Loader_Free::is_pro_active() ? new Rsssl_Provider_Loader_Pro() : new Rsssl_Provider_Loader_Free(); } } wordpress/two-fa/providers/class-rsssl-two-factor-email.php 0000777 00000051001 15251751331 0020147 0 ustar 00 <?php /** * Class for creating an email provider. * * @package Two_Factor */ namespace RSSSL\Security\WordPress\Two_Fa\Providers; /** * Class for creating an email provider. * * @since 7.0.6 * * @package Two_Factor */ require_once rsssl_path . 'mailer/class-mail.php'; use RSSSL\Security\WordPress\Two_Fa\Controllers\Rsssl_Email_Controller; use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Settings; use rsssl_mailer; use Exception; use WP_User; /** * Generate and email the user token. * * @param WP_User $user WP_User object of the logged-in user. * * @return void * @since 0.1-dev */ class Rsssl_Two_Factor_Email extends Rsssl_Two_Factor_Provider implements Rsssl_Two_Factor_Provider_Interface { /** * The user meta token key. * * @var string */ public const RSSSL_TOKEN_META_KEY = '_rsssl_factor_email_token'; /** * Store the timestamp when the token was generated. * * @var string */ public const RSSSL_TOKEN_META_KEY_TIMESTAMP = '_rsssl_factor_email_token_timestamp'; /** * Name of the input field used for code resend. * * @var string */ public const RSSSL_INPUT_NAME_RESEND_CODE = 'rsssl-two-factor-email-code-resend'; public const SECRET_META_KEY = 'rsssl_two_fa_email_enabled'; public const METHOD = 'email'; public const NAME = 'Email'; /** * Ensures only one instance of this class exists in memory at any one time. * * @since 0.1-dev */ public static function get_instance() { static $instance; $class = __CLASS__; if ( ! is_a( $instance, $class ) ) { $instance = new $class(); } return $instance; } /** * Class constructor. * * @since 0.1-dev */ protected function __construct() { add_action( 'rsssl_two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) ); parent::__construct(); } /** * Starts the corresponding controller * @return void */ public static function start_controller(string $namespace, string $version, string $featureVersion ): void { new Rsssl_Email_Controller($namespace, $version, $featureVersion); } /** * Ensure PHP session is started. */ public static function ensure_session_started() { if ( PHP_SAPI !== 'cli' && ! headers_sent() && ! session_id() ) { session_start(); } } /** * Returns the name of the provider. * * @since 0.1-dev */ public function get_label(): string { return _x( 'Email', 'Provider Label', 'really-simple-ssl' ); } /** * Generate the user token. * * @param int $user_id User ID. * * @return string * @since 0.1-dev */ public function generate_token( int $user_id ): string { $token = self::get_code(); update_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY_TIMESTAMP, time() ); update_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY, wp_hash( $token ) ); return $token; } /** * Check if user has a valid token already. * * @param int $user_id User ID. * * @return boolean If user has a valid email token. */ public function user_has_token( int $user_id ): bool { $hashed_token = $this->get_user_token( $user_id ); if ( ! empty( $hashed_token ) ) { return true; } return false; } /** * Has the user token validity timestamp expired. * * @param integer $user_id User ID. * * @return boolean */ public function user_token_has_expired( int $user_id ): bool { $token_lifetime = $this->user_token_lifetime( $user_id ); $token_ttl = $this->user_token_ttl( $user_id ); // Invalid token lifetime is considered an expired token. return ! ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl ); } /** * Get the lifetime of a user token in seconds. * * @param integer $user_id User ID. * * @return integer|null Return `null` if the lifetime can't be measured. */ public function user_token_lifetime( $user_id ) { $timestamp = (int) get_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY_TIMESTAMP, true ); if ( ! empty( $timestamp ) ) { return time() - $timestamp; } return null; } /** * Return the token time-to-live for a user. * * @param integer $user_id User ID. * * @return integer */ public function user_token_ttl( int $user_id ): int { $token_ttl = 15 * MINUTE_IN_SECONDS; /** * Number of seconds the token is considered valid * after the generation. * * @param integer $token_ttl Token time-to-live in seconds. * @param integer $user_id User ID. */ return (int) apply_filters( 'rsssl_two_factor_token_ttl', $token_ttl, $user_id ); } /** * Get the authentication token for the user. * * @param int $user_id User ID. * * @return string|boolean User token or `false` if no token found. */ public function get_user_token( int $user_id ) { $hashed_token = get_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY, true ); if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) { return $hashed_token; } return false; } /** * Validate the user token. * * @param int $user_id User ID. * @param string $token User token. * * @return boolean * @since 0.1-dev */ public function validate_token( int $user_id, string $token ): bool { $hashed_token = $this->get_user_token( $user_id ); // Bail if token is empty or it doesn't match. if ( empty( $hashed_token ) || ! hash_equals( wp_hash( $token ), $hashed_token ) ) { return false; } if ( $this->user_token_has_expired( $user_id ) ) { return false; } // Ensure the token can be used only once. $this->delete_token( $user_id ); update_user_meta( $user_id, 'rsssl_two_fa_status_email', 'active' ); return true; } /** * Delete the user token. * * @param int $user_id User ID. * * @since 0.1-dev */ public function delete_token( int $user_id ): void { delete_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY ); } /** * Generate and email the user token. * * @param WP_User $user WP_User object of the logged-in user. * * @return void * @since 0.1-dev */ public function generate_and_email_token( WP_User $user, $profile = false, $is_resend = false ): void { self::ensure_session_started(); $token = $this->generate_token( $user->ID ); $skip_two_fa_url = Rsssl_Two_Factor_Settings::rsssl_one_time_login_url( $user->ID, false, $profile ); // Add skip button to email content. $skip_button_html = sprintf( '<a href="%s" class="button" style="padding: 10px 30px; background: #2A7ABF; border-color: #2A7ABF; color: #fff; text-decoration: none; text-shadow: none; display: inline-block; margin-top: 15px; font-size: 0.8125rem; font-weight: 300; transition: all .3s ease; min-height: 10px;">' . __( 'Continue', 'really-simple-ssl' ) . '</a>', esc_url( $skip_two_fa_url ) ); /* translators: %s: site name */ $subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'really-simple-ssl' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) ); /* translators: %s: token */ $token_cleaned = wp_strip_all_tags( $token ); // insert whitespace after four characters in the $token, for readability. $token_cleaned = preg_replace( '/(.{4})/', '$1 ', $token_cleaned ); $token_html = sprintf( ' <table cellspacing="0" cellpadding="0" border="0" width="100%%" style="margin-top: 25px;background-color:white; box-shadow: 1px 3px 0 1px rgba(211, 211, 211, 0.3); height: 180px;"> <!-- Further increased height for white box --> <tr> <td style="padding: 45px 10px 10px 10px; vertical-align: middle; font-size: 18px; font-weight:700; text-align: center;">%s</td> <!-- Increased padding for top and bottom --> </tr> <tr> <td style="padding: 10px 20px 45px 20px; vertical-align: middle; text-align: center;">%s</td> <!-- Increased padding for bottom --> </tr> </table>', $token_cleaned, $skip_button_html ); if($profile) { $message = sprintf( __( "Below you'll find the email activation code for %1\$s. It's valid for 15 minutes. %2\$s", 'really-simple-ssl' ), site_url(), $token_html ); } else { $message = sprintf( __( "Below you will find your login code for %1\$s. It's valid for 15 minutes. %2\$s", 'really-simple-ssl' ), site_url(), $token_html ); } /** * Filter the token email subject. * * @param string $subject The email subject line. * @param int $user_id The ID of the user. */ $subject = apply_filters( 'rsssl_two_factor_token_email_subject', $subject, $user->ID ); /** * Filter the token email message. * * @param string $message The email message. * @param string $token The token. * @param int $user_id The ID of the user. */ $message = apply_filters( 'rsssl_two_factor_token_email_message', $message, $token, $user->ID ); if ( ! class_exists( 'rsssl_mailer' ) ) { require_once rsssl_path . 'mailer/class-mail.php'; } $mailer = new rsssl_mailer(); $mailer->subject = $subject; $mailer->branded = false; /* translators: %s is replaced with the site url */ $mailer->sent_by_text = "<b>" . sprintf( __( 'Notification by %s', 'really-simple-ssl' ), site_url() ) . "</b>"; $mailer->template_filename = apply_filters( 'rsssl_email_template', rsssl_path . '/mailer/templates/email-unbranded.html' ); $mailer->to = $user->user_email; $mailer->title = __( 'Hi', 'really-simple-ssl' ) . ' ' . $user->display_name . ','; $mailer->message = $message; $mailer->send_mail(); if ( $is_resend ) { $_SESSION['rsssl_email_resent'] = true; } } public static function maybe_show_email_resend_notice( $user ) { self::ensure_session_started(); if ( ! empty( $_SESSION['rsssl_email_resent'] ) ) { echo '<div class="notice notice-success" style="margin-bottom:16px;"><p>' . esc_html__( 'A new verification code has been sent to your email address.', 'really-simple-ssl' ) . '</p></div>'; unset( $_SESSION['rsssl_email_resent'] ); } } /** * Prints the form that prompts the user to authenticate. * * @param WP_User $user WP_User object of the logged-in user. * * @since 0.1-dev */ public function authentication_page( WP_User $user ): void { if ( ! $user ) { return; } if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) { $this->generate_and_email_token( $user ); } require_once ABSPATH . '/wp-admin/includes/template.php'; ?> <p class="two-factor-prompt"><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'really-simple-ssl' ); ?></p> <p> <label for="rsssl-authcode"><?php esc_html_e( 'Verification Code:', 'really-simple-ssl' ); ?></label> <input type="text" inputmode="numeric" name="rsssl-two-factor-email-code" id="rsssl-authcode" class="input rsssl-authcode" value="" size="20" pattern="[0-9 ]*" placeholder="1234 5678" data-digits="8" /> </p> <?php submit_button( __( 'Log In', 'really-simple-ssl' ), 'primary', 'submit' ); ?> <?php submit_button( __( 'Resend Code', 'really-simple-ssl' ), 'secondary', self::RSSSL_INPUT_NAME_RESEND_CODE ); ?> <script type="text/javascript"> setTimeout( function(){ var d; try{ d = document.getElementById('rsssl-authcode'); d.value = ''; d.focus(); } catch(e){} }, 200); </script> <?php $provider = get_user_meta( $user->ID, 'rsssl_two_fa_status_email', true ); foreach ( $user->roles as $role ) { // Never show the skip link if a role is a forced role. $two_fa_forced_roles = is_array(rsssl_get_option('two_fa_forced_roles')) ? rsssl_get_option('two_fa_forced_roles') : []; if (in_array($role, $two_fa_forced_roles, true)) { break; } // If optional and open, allow the user to skip 2FA for now. if ( 'open' === $provider && in_array( $role, rsssl_get_option( 'two_fa_enabled_roles_email', array() ), true ) ) { $skip_two_fa_url = Rsssl_Two_Factor_Settings::rsssl_one_time_login_url( $user->ID, true ); ?> <a class="rsssl-skip-link" href="<?php echo esc_url( $skip_two_fa_url ); ?>" style="display: flex; justify-content: center; margin: 15px 20px 0 0;"> <?php esc_html_e( "Don't use Two-Factor Authentication", 'really-simple-ssl' ); ?> </a> <?php } } } /** * Send the email code if missing or requested. Stop the authentication * validation if a new token has been generated and sent. * * @param WP_USer $user WP_User object of the logged-in user. * @return boolean */ public function pre_process_authentication( $user ): bool { if ( isset( $user->ID ) && isset( $_REQUEST[ self::RSSSL_INPUT_NAME_RESEND_CODE ] ) ) { $this->generate_and_email_token( $user, false, true ); return true; } return false; } /** * Validates the users input token. * * @param WP_User $user WP_User object of the logged-in user. * @return boolean *@since 0.1-dev * */ public function validate_authentication( WP_User $user ): bool { $code = self::sanitize_code_from_request( 'rsssl-two-factor-email-code' ); if ( ! isset( $user->ID ) || ! $code ) { return false; } return $this->validate_token( $user->ID, $code ); } /** * Whether this Two Factor provider is configured and available for the user specified. * * @param WP_User $user WP_User object of the logged-in user. * @return boolean *@since 0.1-dev * */ public function is_available_for_user( WP_User $user ): bool { return true; } /** * Inserts markup at the end of the user profile field for this provider. * * @param WP_User $user WP_User object of the logged-in user. * * @since 0.1-dev */ public function user_options( WP_User $user ): void { $email = $user->user_email; ?> <div> <?php echo esc_html( sprintf( /* translators: %s: email address */ __( 'Authentication codes will be sent to %s.', 'really-simple-ssl' ), $email ) ); ?> </div> <?php } /** * Check if the user is forced to use two-factor authentication. * * @param WP_User $user The user object. * * @return bool Whether the user is forced to use two-factor authentication. */ public static function is_forced( WP_User $user ): bool { // If there is no user logged in, it can't check if the user is forced. if ( ! $user->exists() ) { return false; } return Rsssl_Two_Factor_Settings::get_role_status( 'email', $user->ID ) === 'forced'; } /** * Check if a user is Optional. * * @param WP_User $user The user object. * * @return bool Whether the user is optional or not. */ public static function is_optional( WP_User $user ): bool { if ( ! $user->exists() ) { return false; } if ( 'disabled' === Rsssl_Two_Factor_Settings::get_user_status( 'email', $user->ID ) ) { return false; } $optional_roles = (array) rsssl_get_option( 'two_fa_enabled_roles_email', array() ); $user_roles = $user->roles; // Guard clause: if roles is not an array, no overlap possible. if ( ! is_array( $user_roles ) ) { return false; } // For multisite, get the strictest role across all sites. if ( is_multisite() ) { $strict_roles = Rsssl_Two_Factor_Settings::get_strictest_role_across_sites( $user->ID, array( 'email' ) ); // Array conversion for possible single-role. if ( is_string( $strict_roles ) ) { $strict_roles = ( '' === $strict_roles ) ? null : array( $strict_roles ); } // No valid roles from multisite, no overlap possible. if ( empty( $strict_roles ) || ! is_array( $strict_roles ) ) { return false; } $user_roles = $strict_roles; } // Check if any of the user's roles are in the optional roles list. return ! empty( array_intersect( $user_roles, $optional_roles ) ); } /** * Set user status for two-factor authentication. * * @param int $user_id User ID. * @param string $status The status to set. * * @return void */ public static function set_user_status( int $user_id, string $status ): void { update_user_meta( $user_id, 'rsssl_two_fa_status_email', $status ); } /** * Returns the HTML for the selection option. * * @param WP_User $user The user object. * @param bool $checked Whether the option is checked or not. * * @return void * @throws Exception Throws an exception if the template file is not found. */ public static function get_selection_option( $user, bool $checked = false ): void { // Get the preferred method meta, which could be a string or an array. $preferred_method_meta = get_user_meta( $user->ID, 'rsssl_two_fa_set_provider', true ); // Normalize the preferred method to always be an array. $preferred_methods = is_array( $preferred_method_meta ) ? $preferred_method_meta : (array) $preferred_method_meta; // Check if 'Rsssl_Two_Factor_Email' is the preferred method. $is_preferred = in_array( 'Rsssl_Two_Factor_Email', $preferred_methods, true ); $is_enabled = (bool) get_user_meta( $user->ID, self::SECRET_META_KEY, true ); $badge_class = $is_enabled ? 'badge-enabled' : 'badge-default'; $enabled_text = $is_enabled ? esc_html__( 'Enabled', 'really-simple-ssl' ) : esc_html__( 'Disabled', 'really-simple-ssl' ); $checked_attribute = $checked ? 'checked' : ''; $title = esc_html__( 'Email', 'really-simple-ssl' ); $description = esc_html__( 'Receive a code by email', 'really-simple-ssl' ); // Check if any of the user's roles are in the forced roles list. $user_roles = ! empty( $user->roles ) && is_array( $user->roles ) ? $user->roles : []; $forced_roles = (array) rsssl_get_option( 'two_fa_forced_roles' ); $is_forcible = ! empty( array_intersect( $user_roles, $forced_roles ) ); // Load the template. rsssl_load_template( 'selectable-option.php', array( 'badge_class' => $badge_class, 'enabled_text' => $enabled_text, 'checked_attribute' => $checked_attribute, 'title' => $title, 'type' => 'email', // Used this to identify the provider. 'forcible' => $is_forcible, 'description' => $description, 'user' => $user, ), rsssl_path . 'assets/templates/two_fa' ); } /** * Check if a user is enabled based on their role. * * @param WP_User $user The user object to check. * * @return bool Whether the user is enabled or not. */ public static function is_enabled( WP_User $user ): bool { // todo - Do we need to check for a pro version here too? if ( ! $user->exists() ) { return false; } // Get the user roles. $user_roles = $user->roles; // Guard clause: if roles is not an array, no overlap possible. if ( ! is_array( $user_roles ) ) { return false; } // For multisite, get the strictest role across all sites. if ( is_multisite() ) { $strict_roles = Rsssl_Two_Factor_Settings::get_strictest_role_across_sites( $user->ID, array( 'email' ) ); // Array conversion for possible single-role. if ( is_string( $strict_roles ) ) { $strict_roles = ( '' === $strict_roles ) ? null : array( $strict_roles ); } // No valid roles from multisite, no overlap possible. if ( empty( $strict_roles ) || ! is_array( $strict_roles ) ) { return false; } $user_roles = $strict_roles; } // Get and normalize enabled roles. $enabled_roles = rsssl_get_option( 'two_fa_enabled_roles_email' ); if ( ! is_array( $enabled_roles ) ) { $enabled_roles = array(); } // Check if any user role is in the enabled roles list. return ! empty( array_intersect( $user_roles, $enabled_roles ) ); } public static function is_configured( WP_User $user ): bool { $status = get_user_meta( $user->ID, 'rsssl_two_fa_status_email', true ); return 'active' === $status; } public static function get_status( WP_User $user ): string { return Rsssl_Two_Factor_Settings::get_user_status( 'email', $user->ID ); } public static function reset_meta_data(int $user_id): void { delete_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY ); delete_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY_TIMESTAMP ); delete_user_meta( $user_id, 'rsssl_two_fa_status_email' ); delete_user_meta( $user_id, 'rsssl_two_fa_skip_token' ); delete_user_meta( $user_id, '_rsssl_factor_email_token_timestamp' ); delete_user_meta( $user_id, '_rsssl_factor_email_token' ); delete_user_meta( $user_id, '_rsssl_two_factor_nonce' ); } } wordpress/two-fa/providers/class-rsssl-provider-loader-free.php 0000777 00000000324 15251751331 0021014 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Providers; class Rsssl_Provider_Loader_Free extends Rsssl_Provider_Loader { public static function get_providers(): array { return parent::get_providers(); } } wordpress/two-fa/contracts/interface-rsssl-two-fa-user-query-builder-interface.php 0000777 00000001335 15251751331 0024516 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Contracts; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters; interface Rsssl_Two_Fa_User_Query_Builder_Interface { /** * Build query args based on data parameters. * * @return array */ public function buildQueryArgs(Rsssl_Two_FA_Data_Parameters $params): array; public function addDisabledConditionToArgs(array $args): array; public function addUnconfigured2FAConditionToArgs(array $args): array; public function addNearingExpiryCondition(array $args, int $daysThreshold, int $reminderBeforeClosingPeriod = 3): array; public function addForcedRolesConditionToArgs(array $args, array $getForcedRoles): array; } wordpress/two-fa/contracts/interface-rsssl-two-fa-user-repository-interface.php 0000777 00000001744 15251751331 0024150 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Contracts; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_Fa_User_Collection; interface Rsssl_Two_Fa_User_Repository_Interface { /** * Retrieve two-factor authentication users based on data parameters. * * @return Rsssl_Two_Fa_User_Collection */ public function getTwoFaUsers(Rsssl_Two_FA_Data_Parameters $params): Rsssl_Two_Fa_User_Collection; /** * Needed for getting all the expired users. * * @return Rsssl_Two_Fa_User_Collection */ public function geTwoFAExpiredUsers(Rsssl_Two_FA_Data_Parameters $params): Rsssl_Two_Fa_User_Collection; /** * Retrieve forced two-factor authentication users with open status. * * @return Rsssl_Two_Fa_User_Collection */ public function getForcedTwoFaUsersWithOpenStatus(Rsssl_Two_FA_Data_Parameters $params): Rsssl_Two_Fa_User_Collection; } wordpress/two-fa/contracts/interface-rsssl-has-processing-interface.php 0000777 00000000610 15251751331 0022476 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Contracts; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_Fa_User_Collection; interface Rsssl_Has_Processing_Interface { /** * Processes a collection of Data Transfer Objects. * @return Rsssl_Two_Fa_User_Collection */ public function processBatch(array $args, string $switchValue): Rsssl_Two_Fa_User_Collection; } wordpress/two-fa/class-rsssl-two-factor-admin.php 0000777 00000024670 15251751331 0016147 0 ustar 00 <?php /** * This file contains the Rsssl_Two_Factor_Admin class. * * The Rsssl_Two_Factor_Admin class is responsible for handling the administrative * aspects of the two-factor authentication feature in the Really Simple SSL plugin. * It includes two_fa_provider for displaying the two-factor authentication settings in the * admin area, handling user input, and managing user roles and capabilities related * to two-factor authentication. * * PHP version 7.2 * * @category Security * @package Really_Simple_SSL * @author Really Simple SSL */ namespace RSSSL\Security\WordPress\Two_Fa; use RSSSL\Security\WordPress\Two_Fa\Controllers\Rsssl_Two_Fa_User_Controller; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters; use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_user; use RSSSL\Security\WordPress\Two_Fa\Repositories\Rsssl_Two_Fa_User_Repository; use RSSSL\Security\WordPress\Two_Fa\Services\Rsssl_Two_Fa_Forced_Role_Service; use RSSSL\Security\WordPress\Two_Fa\Services\Rsssl_Callback_Queue; use RSSSL\Pro\Security\WordPress\Passkey\Models\Rsssl_Webauthn; use WP_User; /** * The Rsssl_Two_Factor_Admin class is responsible for handling the administrative * aspects of the two-factor authentication feature in the Really Simple SSL plugin. * It includes two_fa_provider for displaying the two-factor authentication settings in the * admin area, handling user input, and managing user roles and capabilities related * to two-factor authentication. * * @category Security * @package Really_Simple_SSL * @subpackage Two_Factor */ class Rsssl_Two_Factor_Admin { /** * The Rsssl_Two_Factor_Admin instance. * * @var Rsssl_Two_Factor_Settings $instance The settings object. */ private static $instance; private Rsssl_Callback_Queue $queue; /** * The constructor. * * @return void */ public function __construct() { // if the user is not logged in, it don't need to do anything. if (!rsssl_admin_logged_in()) { return; } if (isset(self::$instance)) { wp_die(); } self::$instance = $this; add_filter('rsssl_do_action', [$this, 'two_fa_table'], 10, 3); add_filter('rsssl_after_save_field', [$this, 'change_disabled_users_when_forced'], 20, 3); add_filter('rsssl_after_save_field', [$this, 'process_added_removed_enabled_roles'], 20, 3); add_filter('rsssl_after_save_field', [$this, 'set_passkey_table'], 20, 3); $this->queue = new Rsssl_Callback_Queue(); $this->queue->process_tasks(1); } /** * Sets the passkey table. */ public function set_passkey_table(string $field_id, $new_value, $prev_value ): void { // checking if the field is the passkey enabled field if ('enable_passkey_login' === $field_id) { // if the passkey is enabled, it needs to set the passkey table. if ($new_value) { new Rsssl_Webauthn(); // Initialize the Webauthn class. It will install everything needed. do_action('rsssl_install_tables'); } //TODO think of what needs to be done when the passkey is disabled. } } /** * Change the disabled status of users when forced. * * @param string $field_id The ID of the field being changed. * @param mixed $new_value The new value of the field. * @param array $prev_value The previous value of the field. * @return void */ public function change_disabled_users_when_forced( string $field_id, $new_value, $prev_value = [] ): void { if ( 'two_fa_forced_roles' !== $field_id || empty($new_value) ) { return; } //making sure that the new value is an array as well as the old value if (!is_array($new_value)) { $new_value = []; } if (!is_array($prev_value)) { $prev_value = []; } $changedRoles = Rsssl_Two_Fa_Forced_Role_Service::getForForcedRolesChange($prev_value, $new_value); // If no roles have changed, return early. if(empty($changedRoles)) { return; } // Set up initial batch parameters. $batch_size = 500; $offset = 0; $params = new Rsssl_Two_FA_Data_Parameters([ 'filter_column' => 'user_role', 'filter_value' => 'all', 'number' => $batch_size, 'offset' => $offset, ]); // Add the first processing task to the queue. $this->queue->add_task([$this, 'process_users_batch'], [ $changedRoles, $params, $batch_size, $offset, 'open' ]); $this->queue->add_task([$this, 'process_users_batch'], [ $changedRoles, $params, $batch_size, $offset, 'disabled']); } /** * Process a batch of forced two-factor users with disabled status. * * @return void */ public function process_users_batch(array $changedRoles, Rsssl_Two_FA_Data_Parameters $params, int $batch_size, int $offset, string $status): void { $collection = (new Rsssl_Two_Fa_Forced_Role_Service($params))->processBatch($changedRoles, $status); foreach ($collection->getUsers() as $user) { $statusForUser = $user->getStatus(); if (in_array($statusForUser, ['open', 'disabled','expired'])) { // Check if the user has a role that has been changed. $rolesForUser = $user->getRoles(); $matchingRoles = array_intersect($rolesForUser, $changedRoles); if (!empty($matchingRoles)) { // Reset the user's status. $user->resetStatus(); //temp meta key for testing update_user_meta($user->getId(), 'rsssl_two_fa_status_reset', true); } } } // Check if there are more users to process. // The collection contains the total number of records (set in the repository). $total = $collection->getTotalRecords(); if (($params->offset + $params->number) < $total) { // Update the offset for the next batch. $newOffset = $offset + $batch_size; // Queue the next task with the correct new offset. $this->queue->add_task([$this, 'process_users_batch'], [$changedRoles, $params, $batch_size, $newOffset, $status]); } } public function process_added_removed_enabled_roles(string $field_id, $new_value, $prev_value = []) { if ( 'two_fa_enabled_roles_email' !== $field_id || empty($new_value) ) { return; } if ( 'two_fa_enabled_roles_totp' !== $field_id || empty($new_value) ) { return; } } /** * Checks if the user can use two-factor authentication (2FA). * * @return bool Returns true if the user can use 2FA, false otherwise. */ public function can_i_use_2fa(): bool { return rsssl_get_option('login_protection_enabled'); } /** * Creates a captcha notice array. * * This method creates and returns an array representing a captcha notice. * * @param string $title The title of the notice. * @param string $msg The message of the notice. * * @return array The captcha notice array. */ private function create_2fa_notice( string $title, string $msg ): array { return array( 'callback' => '_true_', 'score' => 1, 'show_with_options' => array( 'login_protection_enabled' ), 'output' => array( 'true' => array( 'title' => $title, 'msg' => $msg, 'icon' => 'warning', 'type' => 'open', 'dismissible' => true, 'admin_notice' => false, 'plusone' => true, 'highlight_field_id' => 'two_fa_enabled_roles', ), ), ); } /** * Reset the two-factor authentication for a user. * * @param array $response The response array. * @param string $action The action being performed. * @param array $data The data array. * * @return array The updated response array. */ public static function reset_user_two_fa( array $response, string $action, array $data ): array { if ( ! rsssl_user_can_manage() ) { return $response; } if ( 'two_fa_table' === $action ) { // if the user has been disabled, it needs to reset the two-factor authentication. $user = get_user_by( 'id', $data['user_id'] ); if ( $user ) { // Delete all 2fa related user meta. Rsssl_Two_Fa_Status::delete_two_fa_meta( $user->ID ); // Set the last login to now, so the user will be forced to use 2fa. update_user_meta( $user->ID, 'rsssl_two_fa_last_login', gmdate( 'Y-m-d H:i:s' ) ); } } return $response; } /** * Generates the two-factor authentication table data based on the action and data parameters. * * @param array $response The initial response data. * @param string $action The action to perform. * @param array $data The data needed for the action. * * @return array The updated response data. */ public function two_fa_table(array $response, string $action, array $data): array { $new_response = $response; if (rsssl_user_can_manage()) { switch ($action) { case 'two_fa_table': $data_parameters = new Rsssl_Two_FA_Data_Parameters($data); $userRepository = new Rsssl_Two_Fa_User_Repository(); // Create the controller. return (new Rsssl_Two_Fa_User_Controller($userRepository))->getUsersForAdminOverview($data_parameters); case 'two_fa_reset_user': // if the user has been disabled, it needs to reset the two-factor authentication. $user = get_user_by('id', $data['id']); if ($user) { // Delete all 2fa related user meta. Rsssl_Two_Fa_Status::delete_two_fa_meta($user->ID); // Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa. update_user_meta($user->ID, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s')); } if (!$user) { $new_response['request_success'] = false; } break; default: // Default case if no action matches. break; } } return $new_response; } } wordpress/two-fa/assets/js/profile.js 0000777 00000020440 15251751331 0013717 0 ustar 00 class Profile extends BaseAuth { init() { this.assignClickListener('download_codes', this.download_codes); this.assignClickListener('two-factor-qr-code', this.copyTextAndShowMessage); this.assignClickListener('totp-key', this.copyTextAndShowMessage); const qrCodeContainer = this.getElement('qr-code-container'); const enableCheckbox = this.getElement('two-factor-authentication'); const tableRowSelection = this.getElement('selection_two_fa'); const methodSelection = document.querySelectorAll('input[name="preferred_method"]'); const validationEmail = document.getElementById('rsssl_verify_email'); const change2faConfig = this.getElement('change_2fa_config'); let that = this; if (qrCodeContainer) { qrCodeContainer.style.display = "none"; if (!enableCheckbox.checked) { tableRowSelection.style.display = "none"; qrCodeContainer.style.display = "none"; } } if(enableCheckbox) { let parent = this; enableCheckbox.addEventListener("change", function () { if (this.checked) { tableRowSelection.style.display = "table-row"; let selectedMethod = document.querySelector('input[name="preferred_method"]:checked'); if (selectedMethod && selectedMethod.value === "totp") { qrCodeContainer.style.display = "block"; parent.qr_generator(); } else { qrCodeContainer.style.display = "none"; } } else { tableRowSelection.style.display = "none"; qrCodeContainer.style.display = "none"; let selectedMethod = document.querySelector('input[name="preferred_method"]:checked'); selectedMethod.value = "none"; } }); } if(methodSelection.length > 0 ) { let parent = this; methodSelection.forEach(function (element) { element.addEventListener("change", function () { let selectedMethod = document.querySelector('input[name="preferred_method"]:checked').value; if (selectedMethod === "totp") { if(validationEmail) { validationEmail.style.display = "none"; } qrCodeContainer.style.display = "block"; parent.qr_generator(); } else if(selectedMethod === "email") { qrCodeContainer.style.display = "none"; if(validationEmail) { validationEmail.style.display = "table-row"; } let data = { action: 'change_method_to_email', provider: selectedMethod, user_id: rsssl_profile.user_id, login_nonce: document.getElementById('rsssl_two_fa_nonce').value, redirect_to: rsssl_profile.redirect_to, profile: true }; fetch(rsssl_profile.ajax_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: new URLSearchParams(data) }) .then(response => response.json()) .then(responseData => { // Expected structure: { success: true, data: { message: "Verification code sent", token: ... } } let errorDiv = document.getElementById('login-message'); let inPutField = document.getElementById('rsssl-two-factor-email-code'); if (inPutField) { if (!errorDiv) { errorDiv = document.createElement('p'); errorDiv.classList.add('notice', 'notice-success'); inPutField.insertAdjacentElement('afterend', errorDiv); } // Use the message returned from your PHP callback if (responseData.data.message) { errorDiv.innerHTML = `<p>${responseData.data.message}</p>`; } else { console.error('No message returned from the server.'); } // Optionally, do something with responseData.data.token if needed. setTimeout(() => { errorDiv.remove(); }, 5000); } }) .catch(that.logFetchError); } else { qrCodeContainer.style.display = "none"; } }); }); } let resendButton = this.getElement('rsssl_resend_code_action'); if(resendButton !== null) { resendButton.addEventListener('click', (event) => { event.preventDefault(); let data = { action: 'resend_email_code_profile', user_id: this.settings.user_id, login_nonce: document.getElementById('rsssl_two_fa_nonce').value, provider: 'email', profile: true }; let ajaxUrl = rsssl_profile.ajax_url; fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: new URLSearchParams(data) }) .then(response => response.json()) .then(responseData => { // responseData will have the structure: { success: true, data: { message: "..." } } let errorDiv = document.getElementById('login-message'); let inPutField = document.getElementById('rsssl-two-factor-email-code'); if (inPutField) { if (!errorDiv) { errorDiv = document.createElement('p'); errorDiv.classList.add('notice', 'notice-success'); inPutField.insertAdjacentElement('afterend', errorDiv); } errorDiv.innerHTML = `<p>${responseData.data.message}</p>`; // Fade out the message after 5 seconds. setTimeout(() => { errorDiv.remove(); }, 5000); } }) .catch(this.logFetchError); }); } if (change2faConfig) { change2faConfig.addEventListener('click', function (e) { e.preventDefault(); let inputField = document.createElement('input'); inputField.setAttribute('type', 'hidden'); inputField.setAttribute('name', 'change_2fa_config_field'); inputField.setAttribute('value', 'true'); document.getElementById('change_2fa_config').insertAdjacentElement('afterend', inputField); // we uncheck Enable Two-Factor Authentication let enableCheckbox = document.getElementById("two-factor-authentication"); enableCheckbox.checked = false; let profileForm = document.getElementById('your-profile'); if (profileForm) { profileForm.requestSubmit(); } }); } } } wordpress/two-fa/assets/js/onboarding.js 0000777 00000017036 15251751331 0014410 0 ustar 00 class Onboarding extends BaseAuth { init() { const translatableStrings = { keyCopied: 'Key copied', }; let endpoints = ['do_not_ask_again', 'skip_onboarding']; let that = this; endpoints.forEach(endpoint => { let endpointsElement = this.getElement(endpoint); if (endpointsElement !== null) { endpointsElement.addEventListener('click', (event) => { // Use arrow function here event.preventDefault(); // we call the performFetchOp method and then log the response this.performFetchOp(`/${endpoint}`, this.settings) .then(response => response.json()) // We log the data and redirect to the redirect_to URL .then(data => window .location .href = data.redirect_to) // We catch any errors and log them .catch(this.logFetchError); }); } }); let endpointElem = this.getElement('rsssl_continue_onboarding'); const handleClick = (event) => { event.preventDefault(); let urlExtension = ''; let selectedProvider = this.getCheckedInputValue('preferred_method'); if (selectedProvider === 'email') { let data = { provider: selectedProvider, redirect_to: this.settings.redirect_to, user_id: this.settings.user_id, login_nonce: this.settings.login_nonce }; urlExtension = '/save_default_method_email'; this.performFetchOp(urlExtension, data) .then(response => response.json()) .then(data => { this.getElement('rsssl_step_one_onboarding').style.display = 'none'; const validation_check = document.getElementById("rsssl_step_three_onboarding"); validation_check.style.display = "block"; // Removing the 'click' event listener from the rsssl_continue_onboarding id button endpointElem.addEventListener('click', (event) => handleValidation(event, data)); endpointElem.removeEventListener('click', handleClick); }) .catch(that.logFetchError); } else if (selectedProvider === 'totp') { // Hiding step one and showing step two this.getElement('rsssl_step_one_onboarding').style.display = 'none'; // We hide this element endpointElem.style.display = 'none'; this.getElement('rsssl_step_two_onboarding').style.display = 'block'; } } const handleValidation = async (event, data) => { event.preventDefault(); let selectedProvider = this.getCheckedInputValue('preferred_method'); let urlExtension = '/' + data.validation_action; let sendData = { user_id: this.settings.user_id, login_nonce: this.settings.login_nonce, redirect_to: this.settings.redirect_to, token: document.getElementById('rsssl-authcode').value, provider: selectedProvider }; let response; try { response = await this.performFetchOp(urlExtension, sendData); } catch (err) { console.log('Fetch Error: ', err); } if (response && !response.ok) { let error = await response.json(); this.displayTwoFaOnboardingError(error.error); } if (response && response.ok) { let data = await response.json(); window.location.href = data.redirect_to; } }; if (endpointElem !== null) { endpointElem.addEventListener('click', handleClick); } let totpSubmit = this.getElement('two-factor-totp-submit'); if (totpSubmit !== null) { totpSubmit.addEventListener('click', async (event) => { event.preventDefault(); let authCode = document.getElementById('two-factor-totp-authcode').value; let key = this.settings.totp_data.key; let selectedProvider = this.getCheckedInputValue('preferred_method'); let sendData = { 'two-factor-totp-authcode': authCode, provider: selectedProvider, key: key, redirect_to: this.settings.redirect_to, user_id: this.settings.user_id, login_nonce: this.settings.login_nonce }; try { let response = await this.performFetchOp('/save_default_method_totp', sendData); if (!response.ok) { let error = await response.json(); this.displayTwoFaOnboardingError(error.error); } else { let data = await response.json(); window.location.href = data.redirect_to; } } catch (error) { this.logFetchError(error); } }); } let resendButton = this.getElement('rsssl-two-factor-email-code-resend'); if(resendButton !== null) { resendButton.addEventListener('click', (event) => { event.preventDefault(); let data = { user_id: this.settings.user_id, login_nonce: this.settings.login_nonce, provider: 'email' }; this.performFetchOp('/resend_email_code', data) .then(response => response.json()) .then(data => { this.displayTwoFaOnboardingError(data.message); }) .catch(this.logFetchError); }); } let downloadButton = this.getElement('download_codes'); downloadButton.addEventListener('click', (e) => { e.preventDefault(); this.download_codes(); }); this.getElement('two-factor-qr-code').addEventListener('click', function (e) { e.preventDefault(); that.copyTextAndShowMessage(); }); this.getElement('totp-key').addEventListener('click', function (e) { e.preventDefault(); that.copyTextAndShowMessage(); }); if (document.readyState === 'complete') { this.qr_generator(); } else { this.qr_generator(); } } displayTwoFaOnboardingError(error) { let loginForm = document.getElementById('two_fa_onboarding_form'); if (loginForm) { let errorDiv = document.getElementById('login-message'); if(!errorDiv) { errorDiv = document.createElement('div'); errorDiv.id = 'login-message'; errorDiv.className = 'notice notice-error message'; loginForm.insertAdjacentElement('beforebegin', errorDiv); } errorDiv.innerHTML = `<p>${error}</p>`; setTimeout(() => { // removing the error box from the loginForm errorDiv.remove(); }, 5000); } } } wordpress/two-fa/assets/js/BaseAuth.js 0000777 00000010210 15251751331 0013745 0 ustar 00 class BaseAuth { constructor(root, settings) { this.root = root; this.settings = settings; this.translatableStrings = { keyCopied: this.settings.translatables.keyCopied, // ... add more strings as needed }; } getElement = (id) => document.getElementById(id); getCheckedInputValue = (name) => document.querySelector(`input[name="${name}"]:checked`).value; /** * Performs a fetch operation. * * @param {string} urlExtension - The URL extension to perform the fetch operation on. * @param {Object} data - The data to be sent in the fetch operation. * @param {string} [method='POST'] - The HTTP method to be used in the fetch operation. Defaults to 'POST'. * @returns {Promise} - A Promise that resolves with the response of the fetch operation. */ performFetchOp = (urlExtension, data, method = 'POST') => { let url = this.root + urlExtension; let fetchParams = { method: method, headers: {'Content-Type': 'application/json',}, }; if (method === 'POST') { fetchParams.body = JSON.stringify(data); } return fetch(url, fetchParams); }; assignClickListener = (id, callback) => { const element = this.getElement(id); if (element) { element.addEventListener('click', function (e) { e.preventDefault(); callback(); }); } } logFetchError = (error) => console.error('There has been a problem with your fetch operation:', error); /** * Generates a QR code for Two-Factor Authentication using the TOTP URL. * If the TOTP URL is not available, nothing will be generated. * * @function qr_generator * @returns {void} Nothing is returned. */ qr_generator = () => { const totp_url = this.settings.totp_data.totp_url; if (!totp_url) { return; } let qr = qrcode(0, 'L'); qr.addData(totp_url); qr.make(); let qrElem = document.querySelector('#two-factor-qr-code a'); if (qrElem != null) { qrElem.innerHTML = qr.createSvgTag(5); } }; /** * Downloads backup codes as a text file. * * @function download_codes */ download_codes = () => { let TextToCode = this.settings.totp_data.backup_codes; let TextToCodeString = ''; TextToCode.forEach(function (item) { TextToCodeString += item + '\n'; }); let downloadLink = document.createElement('a'); downloadLink.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(TextToCodeString)); downloadLink.setAttribute('download', 'backup_codes.txt'); downloadLink.style.display = 'none'; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); }; /** * This function copies the text from the `totp_data.key` property of the `settings` object * using the Clipboard API. It then shows a success message and reverts back to the original display * after a specified timeout. * * @function copyTextAndShowMessage * @memberof BaseAuth */ copyTextAndShowMessage = () => { let text = this.settings.totp_data.key; // Get the text to be copied // Use Clipboard API to copy the text navigator.clipboard.writeText(text).then(() => { // Change the display of the key let originalText = this.getElement('totp-key').innerText; this.getElement('totp-key').innerText = this.translatableStrings.keyCopied; this.getElement('totp-key').style.color = 'green'; // Revert back to original text after a timeout setTimeout(() => { this.getElement('totp-key').innerText = originalText; this.getElement('totp-key').style.color = ''; // Reset the color }, 2000); // Adjust timeout as needed }, function (err) { console.error(this.settings.translatables.keyCopiedFailed, err); }); } } wordpress/two-fa/assets/js/initialize_two_fa.js 0000777 00000005050 15251751331 0015757 0 ustar 00 /** * The Global rsssl_onboard object is defined in the PHP file that enqueues this script. * @global rsssl_onboard * It contains the following properties: * @typedef {Object} rsssl_onboard * @property {string} root - The root URL of the site. * @property {string} redirect_to - The URL to redirect to after the onboarding process is complete. * @property {string} user_id - The ID of the user. * @property {string} login_nonce - The nonce for the login. * @property {string} totp_data - The data for the TOTP. * @property {string} totp_data.totp_url - The URL for the TOTP. * @property {string} totp_data.backup_codes - The backup codes for the TOTP. * @property {string} totp_data.key - The key for the TOTP. * @property {string} totp_data.authcode - The authcode for the TOTP. * @property {string} totp_data.provider - The provider for the TOTP. * @property {string} totp_data.redirect_to - The URL to redirect to after the TOTP process is complete. */ /** * The Global rsssl_profile object is defined in the PHP file that enqueues this script. * @global rsssl_profile * It contains the following properties: * @typedef {Object} rsssl_profile * @property {string} root - The root URL of the site. * @property {string} redirect_to - The URL to redirect to after the profile process is complete. * @property {string} user_id - The ID of the user. * @property {string} login_nonce - The nonce for the login. * @property {string} totp_data - The data for the TOTP. * @property {string} totp_data.totp_url - The URL for the TOTP. * @property {string} totp_data.backup_codes - The backup codes for the TOTP. * @property {string} totp_data.key - The key for the TOTP. * @property {string} totp_data.authcode - The authcode for the TOTP. * @property {string} totp_data.provider - The provider for the TOTP. * @property {string} totp_data.redirect_to - The URL to redirect to after the TOTP process is complete. * @property {string} totp_data.email - The email for the TOTP. * @property {array} translatables - The translatable strings for the profile. * @property {string} translatables.keyCopied - The message to display when the key is copied. * @property {string} translatables.keyCopiedFailed - The error message to display. */ window.onload = function() { if(typeof rsssl_onboard !== 'undefined') { let onboarding = new Onboarding(rsssl_onboard.root, rsssl_onboard); onboarding.init(); } if (typeof rsssl_profile !== 'undefined') { let profile = new Profile(rsssl_profile.root, rsssl_profile); profile.init(); } } wordpress/two-fa/assets/css/two-fa-onboarding.scss 0000777 00000003043 15251751331 0016307 0 ustar 00 /* Style radio inputs */ .radio-input { position: absolute; right: 0; margin-left: 10px; /* Adjust this value to your preferred spacing */ vertical-align: middle; top: 5px; } /* Style radio labels */ .radio-label { display: inline-block; vertical-align: middle; width: 100%; position: relative; margin: 20px 0; } .badge { margin-left: 10px; padding: 2px 4px; } .badge-default { background-color: #e5e5e5; color: black; } .badge-enabled { background-color: #fbc43e; color: black; } /** * The following styles are for the onboarding form */ #two_fa_onboarding_form { margin-top: 20px; } #two_fa_onboarding_form div { transition: height 0.5s; } #skip_onboarding { margin-right: 20px; } .skip_container { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; a { text-decoration: none; } } .totp-submit { margin-top: 10px; } div.rsssl_step_one_onboarding { display: block; } div.rsssl_step_two_onboarding { display: none; } div.rsssl_step_three_onboarding { margin-top: 10px; display: none; } #two-factor-qr-code { display: flex; /* Enables Flexbox */ justify-content: center; /* Centers horizontally */ align-items: center; /* Centers vertically */ min-width: 205px; min-height: 205px; } .error { color: red; margin-top: -5px; } .input { margin-bottom: 5px !important; } #totp-key { cursor: pointer; display: flex; /* Enables Flexbox */ justify-content: center; /* Centers horizontally */ align-items: center; /* Centers vertically */ } wordpress/two-fa/assets/css/two-fa.scss 0000777 00000000102 15251751331 0014160 0 ustar 00 @import "profile-settings.scss"; @import "two-fa-onboarding.scss"; wordpress/two-fa/assets/css/profile-settings.scss 0000777 00000002014 15251751331 0016265 0 ustar 00 #two-factor-qr-code { display: flex; /* Enables Flexbox */ justify-content: left; /* Centers horizontally */ align-items: center; /* Centers vertically */ width: 100%; min-height: 100%; } #qr-code-container { margin-bottom: 20px; position: relative; text-align: center; //right: 0; } #two-factor-totp-authcode { width: 100%; } tr.rsssl_verify_email { display: none; } .error { color: red; margin-top: -5px; } span.rsssl-backup-codes { padding: 5px; background: #fbebed; border-radius: 8px; box-shadow: rgba(0,0,0,0.1) 0 4px 6px -1px; } .input { margin-bottom: 5px !important; } #totp-key { cursor: pointer; display: flex; /* Enables Flexbox */ justify-content: center; /* Centers horizontally */ align-items: center; /* Centers vertically */ } table.rsssl-table-two-fa { padding-bottom: 20px; } .rsssl-methods-tag { padding: 2px 5px; border: 1px solid #000; color: #000; margin-left: 5px; background: dimgrey; &.active { background: darkgreen; color: #fff; } } wordpress/two-fa/function-login-header.php 0000777 00000017026 15251751331 0014703 0 ustar 00 <?php /** * Extracted from wp-login.php since that file also loads WP core which already have. * * @package REALLY_SIMPLE_SSL */ /** * Output the login page header. * * @param string $title Optional. WordPress login Page title to display in the `<title>` element. * Default 'Log In'. * @param string $message Optional. Message to display in header. Default empty. * @param WP_Error|null $wp_error Optional. The error to pass. Default is a WP_Error instance. * * @global string $action The action that brought the visitor to the login page. * * @since 2.1.0 * * @global string $error Login error message set by deprecated pluggable wp_login() function * or plugins replacing it. * @global bool|string $interim_login Whether interim login modal is being displayed. String 'success' * upon successful login. */ function login_header( string $title = 'Log In', string $message = '', WP_Error $wp_error = null ) { global $error, $interim_login, $action; // Don't index any of these forms. add_filter( 'wp_robots', 'wp_robots_sensitive_page' ); add_action( 'login_head', 'wp_strict_cross_origin_referrer' ); add_action( 'login_head', 'wp_login_viewport_meta' ); if ( ! is_wp_error( $wp_error ) ) { $wp_error = new WP_Error(); } // Shake it! $shake_error_codes = array( 'empty_password', 'empty_email', 'invalid_email', 'invalidcombo', 'empty_username', 'invalid_username', 'incorrect_password', 'retrieve_password_email_failure' ); /** * Filters the error codes array for shaking the login form. * * @since 3.0.0 * * @param array $shake_error_codes Error codes that shake the login form. */ $shake_error_codes = apply_filters( 'shake_error_codes', $shake_error_codes ); if ( $shake_error_codes && $wp_error->has_errors() && in_array( $wp_error->get_error_code(), $shake_error_codes, true ) ) { add_action( 'login_footer', 'wp_shake_js', 12 ); } $login_title = get_bloginfo( 'name', 'display' ); /* translators: Login screen title. 1: Login screen name, 2: Network or site name. */ $login_title = sprintf( __( '%1$s ‹ %2$s — WordPress' ), $title, $login_title ); if ( wp_is_recovery_mode() ) { /* translators: %s: Login screen title. */ $login_title = sprintf( __( 'Recovery Mode — %s' ), $login_title ); } /** * Filters the title tag content for login page. * * @since 4.9.0 * * @param string $login_title The page title, with extra context added. * @param string $title The original page title. */ $login_title = apply_filters( 'login_title', $login_title, $title ); ?><!DOCTYPE html> <html <?php language_attributes(); ?>> <head> <meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php bloginfo( 'charset' ); ?>" /> <title><?php echo esc_html( $login_title ); ?></title> <?php wp_enqueue_style( 'login' ); /* * Remove all stored post data on logging out. * This could be added by add_action('login_head'...) like wp_shake_js(), * but maybe better if it's not removable by plugins. */ if ( 'loggedout' === $wp_error->get_error_code() ) { ?> <script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script> <?php } /** * Enqueue scripts and styles for the login page. * * @since 3.1.0 */ do_action( 'login_enqueue_scripts' ); /** * Fires in the login page header after scripts are enqueued. * * @since 2.1.0 */ do_action( 'login_head' ); $login_header_url = __( 'https://wordpress.org/' ); /** * Filters link URL of the header logo above login form. * * @since 2.1.0 * * @param string $login_header_url Login header logo URL. */ $login_header_url = apply_filters( 'login_headerurl', $login_header_url ); $login_header_title = ''; /** * Filters the title attribute of the header logo above login form. * * @since 2.1.0 * @deprecated 5.2.0 Use {@see 'login_headertext'} instead. * * @param string $login_header_title Login header logo title attribute. */ $login_header_title = apply_filters_deprecated( 'login_headertitle', array( $login_header_title ), '5.2.0', 'login_headertext', __( 'Usage of the title attribute on the login logo is not recommended for accessibility reasons. Use the link text instead.' ) ); $login_header_text = empty( $login_header_title ) ? __( 'Powered by WordPress' ) : $login_header_title; /** * Filters the link text of the header logo above the login form. * * @since 5.2.0 * * @param string $login_header_text The login header logo link text. */ $login_header_text = apply_filters( 'login_headertext', $login_header_text ); $classes = array( 'login-action-' . $action, 'wp-core-ui' ); if ( is_rtl() ) { $classes[] = 'rtl'; } if ( $interim_login ) { $classes[] = 'interim-login'; ?> <style type="text/css">html{background-color: transparent;}</style> <?php if ( 'success' === $interim_login ) { $classes[] = 'interim-login-success'; } } $classes[] = ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) ); /** * Filters the login page body classes. * * @since 3.5.0 * * @param array $classes An array of body classes. * @param string $action The action that brought the visitor to the login page. */ $classes = apply_filters( 'login_body_class', $classes, $action ); ?> </head> <body class="login no-js <?php echo esc_attr( implode( ' ', $classes ) ); ?>"> <script type="text/javascript"> document.body.className = document.body.className.replace('no-js','js'); </script> <?php /** * Fires in the login page header after the body tag is opened. * * @since 4.6.0 */ do_action( 'login_header' ); ?> <div id="login"> <h1><a href="<?php echo esc_url( $login_header_url ); ?>"><?php echo esc_html( $login_header_text ); ?></a></h1> <?php /** * Filters the message to display above the login form. * * @since 2.1.0 * * @param string $message Login message text. */ $message = apply_filters( 'login_message', $message ); if ( ! empty( $message ) ) { echo esc_html( $message ) . "\n"; } // In case a plugin uses $error rather than the $wp_errors object. if ( ! empty( $error ) ) { $wp_error->add( 'error', $error ); unset( $error ); } if ( $wp_error->has_errors() ) { $errors = ''; $messages = ''; foreach ( $wp_error->get_error_codes() as $code ) { $severity = $wp_error->get_error_data( $code ); foreach ( $wp_error->get_error_messages( $code ) as $error_message ) { if ( 'message' === $severity ) { $messages .= ' ' . $error_message . "<br />\n"; } else { $errors .= ' ' . $error_message . "<br />\n"; } } } if ( ! empty( $errors ) ) { /** * Filters the error messages displayed above the login form. * * @since 2.1.0 * * @param string $errors Login error message. */ echo '<div id="login_error">' . esc_html( apply_filters( 'login_errors', $errors ) ) . "</div>\n"; } if ( ! empty( $messages ) ) { /** * Filters instructional messages displayed above the login form. * * @since 2.5.0 * * @param string $messages Login messages. */ echo '<p class="message">' . esc_html( apply_filters( 'login_messages', $messages ) ) . "</p>\n"; } } } // End of login_header(). /** * Outputs the viewport meta tag for the login page. * * @since 3.7.0 */ function wp_login_viewport_meta() { ?> <meta name="viewport" content="width=device-width" /> <?php } wordpress/two-fa/models/class-rsssl-request-parameters.php 0000777 00000013354 15251751331 0020105 0 ustar 00 <?php /** * Holds the request parameters for a specific action. * * @package REALLY_SIMPLE_SSL */ namespace RSSSL\Security\WordPress\Two_Fa\Models; use RSSSL\Pro\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Passkey; use WP_REST_Request; use WP_User; /** * Class Rsssl_Request_Parameters * * This class holds the request parameters for a specific action. * It is used to store the parameters and pass them to the functions. * * @package REALLY_SIMPLE_SSL */ class Rsssl_Request_Parameters { /** * User ID. * * @var int */ public int $user_id; /** * Login nonce. * * @var string */ public string $login_nonce; /** * User object. * * @var WP_User|null */ public ?WP_User $user = null; /** * Service provider. * * @var string|object */ public string $provider; /** * Redirect URL. * * @var string */ public string $redirect_to; /** * Authentication code. * * @var string */ public string $code; /** * Authentication key. * * @var string */ public string $key; /** * Nonce value. * * @var mixed|null */ public string $nonce; /** * Authentication token. * * @var string */ public string $token; /** * Passkey ID. * * @var string */ public string $id; /** * Raw ID for passkey. * * @var string */ public string $rawId; /** * Response data. * * @var array */ public array $response; /** * Request type. * * @var string */ public string $type; /** * Unique browser identifier. * * @var string */ public string $unique_browser_identifier; /** * User login. * * @var string */ public string $user_login; /** * User handle. * * @var mixed|null */ public string $user_handle; /** * Onboarding flag. * * @var bool */ public bool $onboarding; /** * Auth device ID. * * @var string */ public string $auth_device_id; public int $entry_id; public bool $profile; public array $forced_roles = []; public int $days_threshold = 0; /** * Constructor for the class. * * @param WP_REST_Request $request The WordPress REST request object. */ public function __construct( WP_REST_Request $request ) { $this->initialize_parameters( $request ); } /** * Initialize the class properties based on the request parameters. * * @param WP_REST_Request $request The WordPress REST request object. */ private function initialize_parameters( WP_REST_Request $request ): void { $allowed_providers = array( 'passkey', 'email', 'totp', 'passkey_register' ); $this->nonce = sanitize_text_field( $request->get_header( 'X-WP-Nonce' ) ); $this->redirect_to = $request->get_param( 'redirect_to' ) ? wp_validate_redirect( $request->get_param( 'redirect_to' ), admin_url() ) : admin_url(); $this->login_nonce = sanitize_text_field( $request->get_param( 'login_nonce' ) ); $provider = $request->get_param( 'provider' ); $this->forced_roles = rsssl_get_option( 'two_fa_forced_role' , [] ); $this->days_threshold = rsssl_get_option( 'two_fa_days_threshold', 0 ); if ( ! in_array( $provider, $allowed_providers, true ) ) { $provider = null; } if ( $request->has_param( 'credential' ) || $request->has_param( 'credentials' ) ) { $this->initialize_passkey_parameters( $request ); } else { $this->user_id = $request->get_param( 'user_id' )?? 0; $this->provider = $provider?? 'none'; $user = get_user_by( 'id', $this->user_id ); if ($user) { $this->user = $user; } if ($request->has_param('entry_id')) { $this->entry_id = (int) $request->get_param('entry_id'); } } if ( $provider === 'totp' ) { $this->code = sanitize_text_field( wp_unslash( $request->get_param( 'two-factor-totp-authcode' ) ) ); $this->key = sanitize_text_field( wp_unslash( $request->get_param( 'key' ) ) ); } if ( $provider === 'email' ) { $this->token = sanitize_text_field( wp_unslash( $request->get_param( 'token' ) ) ); $this->profile = wp_unslash( $request->get_param( 'profile' ) ?? false ); } $this->unique_browser_identifier = sanitize_text_field( $request->get_param( 'unique_browser_identifier' ) ); $this->user_login = sanitize_user( wp_unslash( $request->get_param( 'user_login' ) ) ); $this->user_handle = sanitize_text_field( $request->get_param( 'userHandle' ) ); $this->onboarding = (bool) $request->get_param( 'onboarding' ); $this->auth_device_id = sanitize_text_field( $request->get_param( 'device_name' ) ?? 'unknown' ); // If user_id is set, we try to get the user object. if ( $this->user_id ) { $user = get_user_by( 'id', $this->user_id ); if ($user) { $this->user = $user; } return; } // If user_login is set, we try to get the user object by login. Since we probably are in the login flow, // we want to get the user by login. if ( $this->user_login ) { $user = get_user_by( 'login', $this->user_login ); if ( $user ) { $this->user_id = $user->ID; $this->user = $user; } } } /** * Initialize passkey-specific parameters. * * @param WP_REST_Request $request The WordPress REST request object. */ private function initialize_passkey_parameters( WP_REST_Request $request ): void { $this->user_id = $request->get_param( 'user_id' ) ? absint( $request->get_param( 'user_id' ) ) : get_current_user_id(); $this->provider = Rsssl_Two_Factor_Passkey::class; $this->id = sanitize_text_field( $request->get_param( 'id' ) ); $this->rawId = sanitize_text_field( $request->get_param( 'rawId' ) ); if( !$request->has_param( 'credentials' ) ) { //To do regex sanitation $this->response = $request->get_param( 'credential' ); } $this->type = sanitize_text_field( $request->get_param( 'type' ) ); $this->entry_id = (int) $request->get_param( 'entry_id' ); } } wordpress/two-fa/models/class-rsssl-two-factor-user-factory.php 0000777 00000010466 15251751331 0020763 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Models; use RSSSL\Security\WordPress\Two_Fa\Services\Rsssl_Two_Fa_Status_Service; use WP_User; class Rsssl_Two_Factor_User_Factory { /** * Defines a role hierarchy. * * @var array<string, int> */ protected array $roleHierarchy = [ 'administrator' => 100, 'editor' => 80, 'author' => 60, 'subscriber' => 40, 'contributor' => 20, ]; private Rsssl_Two_Fa_Status_Service $statusService; /** * Inject the status service. * */ public function __construct() { $this->statusService = new Rsssl_Two_Fa_Status_Service(); } /** * Create a TwoFaUser from a WP_User object. * * @return Rsssl_Two_FA_user|null Returns null if the user has no roles. */ public function createFromWPUser( WP_User $user, array $forcedRoles, array $enabledRoles, int $daysThreshold ): ?Rsssl_Two_FA_user { // Retrieve user roles. $userRoles = $user->roles; if ( is_multisite() && empty( $userRoles ) ) { $userRoles = []; // On multisite, roles are stored per site under `<blog_prefix>capabilities` user meta. // get_blogs_of_user() does not reliably include the role; fetch capabilities per site instead. $blogs = get_blogs_of_user( $user->ID ); if ( ! empty( $blogs ) && is_array( $blogs ) ) { global $wpdb; foreach ( $blogs as $blog ) { // Determine a blog ID property that exists on this object/array. $blogId = 0; if ( is_object( $blog ) ) { $blogId = isset( $blog->userblog_id ) ? (int) $blog->userblog_id : ( isset( $blog->blog_id ) ? (int) $blog->blog_id : ( isset( $blog->id ) ? (int) $blog->id : 0 ) ); } elseif ( is_array( $blog ) ) { $blogId = isset( $blog['userblog_id'] ) ? (int) $blog['userblog_id'] : ( isset( $blog['blog_id'] ) ? (int) $blog['blog_id'] : ( isset( $blog['id'] ) ? (int) $blog['id'] : 0 ) ); } if ( $blogId > 0 ) { $prefix = $wpdb->get_blog_prefix( $blogId ); $caps = get_user_meta( $user->ID, $prefix . 'capabilities', true ); if ( is_array( $caps ) ) { // Collect roles where the capability is truthy. $rolesForSite = array_keys( array_filter( $caps ) ); if ( ! empty( $rolesForSite ) ) { $userRoles = array_merge( $userRoles, $rolesForSite ); } } } } } // Fall back for network admins who may not have per-site roles. if ( function_exists( 'is_super_admin' ) && is_super_admin( $user->ID ) ) { $userRoles[] = 'administrator'; } $userRoles = array_values( array_unique( $userRoles ) ); } if ( empty( $userRoles ) ) { return null; } // Use the status service to determine the user's status. $statusForUser = $this->statusService->determineStatus( $user->ID, $forcedRoles, $daysThreshold ); // Determine two-factor provider. $provider = $this->determineTwoFaProvider( $user->ID ); // Identify matching roles. $matchingRoles = array_intersect( $userRoles, $enabledRoles ); // If multiple roles exist and one of them is forced, prefer the forced role. if ( ! empty( $forcedRoles ) && count( $matchingRoles ) > 1 ) { $matchingForcedRoles = array_intersect( $matchingRoles, $forcedRoles ); if ( ! empty( $matchingForcedRoles ) ) { $matchingRoles = $matchingForcedRoles; } } // If multiple roles remain, choose the most important one based on the defined hierarchy. if ( count( $matchingRoles ) > 1 ) { usort( $matchingRoles, function ( $role1, $role2 ) { $priority1 = $this->roleHierarchy[ $role1 ] ?? 0; $priority2 = $this->roleHierarchy[ $role2 ] ?? 0; return $priority2 <=> $priority1; } ); } // Determine the most important role. $mostImportantRole = reset( $matchingRoles ); return new Rsssl_Two_FA_user ( $user->ID, $user->user_login, $statusForUser, $provider, $userRoles ); } /** * Determine the active two-factor provider. */ protected function determineTwoFaProvider( int $userId ): string { $providers = [ [ 'provider' => 'totp', 'meta_key' => 'rsssl_two_fa_status_totp' ], [ 'provider' => 'email', 'meta_key' => 'rsssl_two_fa_status_email' ], [ 'provider' => 'passkey', 'meta_key' => 'rsssl_two_fa_status_passkey' ], ]; foreach ( $providers as $entry ) { if ( get_user_meta( $userId, $entry['meta_key'], true ) === 'active' ) { return $entry['provider']; } } return 'none'; } } wordpress/two-fa/models/class-rsssl-two-fa-user.php 0000777 00000004535 15251751331 0016426 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Models; use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Status; use RSSSL\Security\WordPress\Two_Fa\Services\Rsssl_Two_Fa_Status_Service; class Rsssl_Two_FA_user { private int $id; private string $username; private string $status; private string $provider; private array $roles; private bool $canResetStatus; public function __construct(int $id, string $username, string $status, string $provider, array $roles) { $this->id = $id; $this->username = $username; $this->status = $status; $this->provider = $provider; $this->roles = $roles; $this->canResetStatus = $this->isStatusResettable(); } // Getter methods /** * Get the user ID. * @return int */ public function getId(): int { return $this->id; } /** * Get the username. * @return string */ public function getUsername(): string { return $this->username; } /** * Get the status. * * @return string */ public function getStatus(): string { return $this->status; } /** * Get the provider. * * @return string */ public function getProvider(): string { return $this->provider; } /** * Get the roles. * * @return array */ public function getRoles(): array { return $this->roles; } /** * Checks if the status is resettable. * * @return bool */ public function isStatusResettable(): bool { // array of statuses that can be reset $resettableStatuses = ['expired', 'disabled', 'active']; // if the status is in the array, return true or false. return $this->canResetStatus = in_array($this->status, $resettableStatuses); } /** * Resets te status of the user. */ public function resetStatus(): void { Rsssl_Two_Fa_Status::delete_two_fa_meta( $this->id ); // Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa. update_user_meta( $this->id, 'rsssl_two_fa_last_login', gmdate( 'Y-m-d H:i:s' ) ); // New: We add also a reset for the passkey meta key, so the user can reconfigure it. delete_user_meta( $this->id, 'rsssl_passkey_configured'); } } wordpress/two-fa/models/class-rsssl-two-fa-user-collection.php 0000777 00000002047 15251751331 0020553 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Models; class Rsssl_Two_Fa_User_Collection { /** * An array to hold TwoFaUser objects. * * @var Rsssl_Two_FA_user[] */ private array $users = []; /** * The total number of records (useful for pagination). * * @var int */ private int $totalRecords = 0; /** * Add a TwoFaUser to the collection. */ public function add(Rsssl_Two_FA_user $user): void { $this->users[] = $user; } /** * Retrieve all TwoFaUser objects in the collection. * * @return Rsssl_Two_FA_user[] */ public function getUsers(): array { return $this->users; } /** * Set the total number of records. */ public function setTotalRecords(int $totalRecords): void { $this->totalRecords = $totalRecords; } /** * Get the total number of records. * * @return int */ public function getTotalRecords(): int { return $this->totalRecords; } } wordpress/two-fa/models/class-rsssl-two-fa-data-parameters.php 0000777 00000011471 15251751331 0020517 0 ustar 00 <?php namespace RSSSL\Security\WordPress\Two_Fa\Models; use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Settings; use WP_Roles as WP_RolesAlias; class Rsssl_Two_FA_Data_Parameters { // Other properties initialized in your constructor... public int $page; public int $page_size; public string $search_term; public ?string $filter_value; public ?string $filter_column; public ?string $sort_column; public string $sort_direction; public string $method; public int $number; public int $offset; public int $negative_count; public string $role_filter; // Optional properties are declared as private and nullable. public ?array $enabled_roles = null; public ?array $forced_roles = null; public ?int $days_threshold = null; public function __construct( array $data ) { // Your existing initialization logic... $this->page = isset($data['currentPage']) ? (int)$data['currentPage'] : 1; $this->page_size = isset($data['currentRowsPerPage']) ? (int)$data['currentRowsPerPage'] : 5; $this->search_term = isset($data['search']) ? sanitize_text_field($data['search']) : ''; $allowed_filters = array_map('strtolower', array_values((new WP_RolesAlias())->get_names())); $this->filter_value = in_array($data['filterValue'] ?? 'all', $allowed_filters, true) ? sanitize_text_field($data['filterValue'] ?? 'all') : 'all'; $this->sort_direction = in_array(strtoupper($data['sortDirection'] ?? 'DESC'), ['ASC', 'DESC'], true) ? strtoupper(sanitize_text_field($data['sortDirection'] ?? 'DESC')) : 'DESC'; $this->filter_column = isset($data['filterColumn']) ? sanitize_text_field($data['filterColumn']) : 'user_role'; $this->sort_column = isset($data['sortColumn']) ? sanitize_text_field($data['sortColumn']) : 'user'; $this->method = isset($data['method']) ? Rsssl_Two_Factor_Settings::sanitize_method($data['method']) : 'email'; $this->number = isset($data['number']) ? (int)$data['number'] : 100; $this->offset = isset($data['offset']) ? (int)$data['offset'] : 0; $this->negative_count = isset($data['negative_count']) ? (int)$data['negative_count'] : 0; $this->role_filter = isset($data['role_filter']) ? sanitize_text_field($data['role_filter']) : 'all'; } /** * Lazy getter for enabled roles. */ public function getEnabledRoles(): array { if ($this->enabled_roles === null) { // if the passkey is enabled all roles are enabled if (defined('rsssl_pro') && rsssl_get_option('enable_passkey_login', false)) { $this->enabled_roles = array_map('strtolower', array_values((new WP_RolesAlias())->get_names())); } else { $this->enabled_roles = array_unique(array_merge( defined('rsssl_pro') ? rsssl_get_option('two_fa_enabled_roles_totp', []) : [], rsssl_get_option('two_fa_enabled_roles_email', []) )); } } return $this->enabled_roles; } /** * Lazy getter for forced roles. */ public function getForcedRoles(): array { if ($this->forced_roles === null) { $this->forced_roles = rsssl_get_option('two_fa_forced_roles', []); } return $this->forced_roles; } /** * Lazy getter for days threshold. */ public function getDaysThreshold(): int { if ($this->days_threshold === null) { $this->days_threshold = (int) rsssl_get_option('two_fa_grace_period', 30); } return $this->days_threshold; } /** * Set the number of items to retrieve. * * @return Rsssl_Two_FA_Data_Parameters */ public function setOffset(int $offset): self { $this->offset = $offset; return $this; } /** * Set the number of items to retrieve. * * @return Rsssl_Two_FA_Data_Parameters */ public function setNumber(int $batch_size): self { $this->number = $batch_size; return $this; } public function toArray() { return [ 'currentPage' => $this->page, 'currentRowsPerPage' => $this->page_size, 'search' => $this->search_term, 'filterValue' => $this->filter_value, 'filterColumn' => $this->filter_column, 'sortColumn' => $this->sort_column, 'sortDirection' => $this->sort_direction, 'method' => $this->method, 'number' => $this->number, 'offset' => $this->offset, 'negative_count' => $this->negative_count, 'role_filter' => $this->role_filter, ]; } } wordpress/hide-wp-version.php 0000777 00000005205 15251751331 0012341 0 ustar 00 <?php defined( 'ABSPATH' ) or die(); if ( ! class_exists( 'rsssl_hide_wp_version' ) ) { class rsssl_hide_wp_version { private static $_this; public $new_version = false; function __construct() { if ( isset( self::$_this ) ) { wp_die( "you cannot create a second instance of a singleton class" ); } self::$_this = $this; add_action( 'init', array($this, 'remove_wp_version') ); add_filter( 'rsssl_fixer_output', array( $this, 'replace_wp_version') ); } static function this() { return self::$_this; } /** * Remove WordPress version info from page source * * @return void */ public function remove_wp_version() { // remove <meta name="generator" content="WordPress VERSION" /> add_filter( 'the_generator', function () { return ''; } ); // remove WP ?ver=5.X.X from css/js add_filter( 'style_loader_src', array( $this, 'remove_css_js_version' ), 9999 ); add_filter( 'script_loader_src', array ($this, 'remove_css_js_version'), 9999 ); remove_action( 'wp_head', 'wp_generator' ); // remove wordpress version remove_action( 'wp_head', 'index_rel_link' ); // remove link to index page remove_action( 'wp_head', 'wlwmanifest_link' ); // remove wlwmanifest.xml (needed to support windows live writer) remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 ); // Remove shortlink } /** * Generate a random version number * * @return string */ public function generate_rand_version() { if ( !$this->new_version) { $wp_version = get_bloginfo( 'version' ); $token = get_option( 'rsssl_wp_version_token' ); if ( ! $token ) { $token = str_shuffle( time() ); update_option( 'rsssl_wp_version_token', $token ); } $this->new_version = hash( 'md5', $token ); } return $this->new_version; } /** * @param string $html * * @return string * */ public function replace_wp_version( $html ) { $wp_version = get_bloginfo( 'version' ); $new_version = $this->generate_rand_version(); return str_replace( '?ver=' . $wp_version, '?ver=' . $new_version, $html ); } /** * @param $src * * @return mixed|string * Remove WordPress version from css and js strings */ public function remove_css_js_version( $src ) { if ( empty($src) ) { return $src; } if ( strpos( $src, '?ver=' ) && strpos( $src, 'wp-includes' ) ) { $wp_version = get_bloginfo( 'version' ); $new_version = $this->generate_rand_version(); $src = str_replace( '?ver=' . $wp_version, '?ver=' . $new_version, $src ); } return $src; } } } RSSSL_SECURITY()->components['hide-wp-version'] = new rsssl_hide_wp_version(); wordpress/user-enumeration.php 0000777 00000004627 15251751331 0012632 0 ustar 00 <?php defined('ABSPATH') or die(); /** * Prevent User Enumeration * @return void */ function rsssl_check_user_enumeration() { if ( ! is_user_logged_in() && isset( $_REQUEST['author'] ) ) { if ( preg_match( '/\\d/', $_REQUEST['author'] ) > 0 ) { wp_die( sprintf(__( 'forbidden - number in author name not allowed = %s', 'really-simple-ssl' ), esc_html( $_REQUEST['author'] ) ) ); } } } add_action('init', 'rsssl_check_user_enumeration'); /** * @return bool * Remove author from Yoast sitemap */ function rsssl_remove_author_from_yoast_sitemap( $users ) { return false; } add_filter('wpseo_sitemap_exclude_author', 'rsssl_remove_author_from_yoast_sitemap', 10, 1 ); /** * Prevent WP JSON API User Enumeration * Return 401 Unauthorized */ if ( !is_user_logged_in() || !current_user_can('edit_posts') ) { add_filter( 'rest_endpoints', function ( $endpoints ) { if ( isset( $endpoints['/wp/v2/users'] ) ) { // Save the original endpoint $original_endpoint = $endpoints['/wp/v2/users']; // Override the GET callback $endpoints['/wp/v2/users'][0]['callback'] = function() { return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you are not allowed to access users without authentication.', 'really-simple-ssl' ), array( 'status' => 401 ) ); }; // Preserve the original args and permission callback $endpoints['/wp/v2/users'][0]['args'] = $original_endpoint[0]['args']; $endpoints['/wp/v2/users'][0]['permission_callback'] = '__return_true'; } if ( isset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] ) ) { // Save the original endpoint $original_endpoint = $endpoints['/wp/v2/users/(?P<id>[\d]+)']; // Override the GET callback $endpoints['/wp/v2/users/(?P<id>[\d]+)'][0]['callback'] = function() { return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you are not allowed to access user data without authentication.', 'really-simple-ssl' ), array( 'status' => 401 ) ); }; // Preserve the original args and permission callback $endpoints['/wp/v2/users/(?P<id>[\d]+)'][0]['args'] = $original_endpoint[0]['args']; $endpoints['/wp/v2/users/(?P<id>[\d]+)'][0]['permission_callback'] = '__return_true'; } return $endpoints; } ); } //prevent xml site map user enumeration add_filter( 'wp_sitemaps_add_provider', function( $provider, $name ) { if ( 'users' === $name ) { return false; } return $provider; }, 10, 2 ); wordpress/vulnerabilities.php 0000777 00000175631 15251751331 0012535 0 ustar 00 <?php use security\wordpress\vulnerabilities\Rsssl_File_Storage; defined('ABSPATH') or die(); //including the file storage class require_once(rsssl_path . 'security/wordpress/vulnerabilities/class-rsssl-file-storage.php'); /** * @package Really Simple Security * @subpackage RSSSL_VULNERABILITIES */ if (!class_exists("rsssl_vulnerabilities")) { /** * * Class rsssl_vulnerabilities * Checks for vulnerabilities in the core, plugins and themes. * * @property $notices * @author Marcel Santing * this class handles import of vulnerabilities, notifying and informing the user. * */ class rsssl_vulnerabilities { const RSSSL_SECURITY_API = 'https://downloads.really-simple-security.com/rsssl/vulnerabilities/V1/'; public $workable_plugins = []; /** * interval to download new jsons */ public $interval = 12 * HOUR_IN_SECONDS; public $update_count = 0; protected $risk_naming = []; /** * @var array|int[] */ public $risk_levels = [ 'l' => 1, 'm' => 2, 'h' => 3, 'c' => 4, ]; public $jsons_files_updated = false; public function __construct() { $this->init(); $this->load_translations_just_in_time(); add_filter('rsssl_vulnerability_data', array($this, 'get_stats')); //now we add the action to the cron. add_action('rsssl_three_hours_cron', array($this, 'run_cron')); add_filter('rsssl_notices', [$this, 'show_help_notices'], 10, 1); add_action( 'rsssl_after_save_field', array( $this, 'maybe_delete_local_files' ), 10, 4 ); add_action( 'rsssl_upgrade', array( $this, 'upgrade_encrypted_files') ); } /** * As this class is not only instantiated by requiring this file * but also in other class instances, we are not 100% sure of the * current filter or action. So we check if we are in the init action * or if the init action has already been executed. If so, we load the * translations immediately and just in time. */ public function load_translations_just_in_time(): void { add_action('init', [$this, 'load_translations']); if (current_filter() === 'init' || did_action('init') > 0) { $this->load_translations(); } } /** * Load the translations for the risk levels */ public function load_translations(): void { $this->risk_naming = [ 'l' => __('low-risk', 'really-simple-ssl'), 'm' => __('medium-risk', 'really-simple-ssl'), 'h' => __('high-risk', 'really-simple-ssl'), 'c' => __('critical', 'really-simple-ssl'), ]; } /** * Upgrade to the new encryption system by deleting all files en re-downloading * * @return void */ public function upgrade_encrypted_files($prev_version) : void { if ( $prev_version && version_compare( $prev_version, '8.3.0', '<' ) ) { //delete all files and reload Rsssl_File_Storage::DeleteAll(); $this->force_reload_files(); delete_option( 'rsssl_hashkey' ); } } // /** // * @param $field_id // * @param $field_value // * @param $prev_value // * @param $field_type // * // * @return void // * // * // */ // public function maybe_enable_vulnerability_scanner( $field_id, $field_value, $prev_value, $field_type ) { // if ( $field_id==='enable_vulnerability_scanner' && $field_value !== $prev_value && rsssl_user_can_manage() ) { // if ( $field_value !== false ) { // // Already enabled // rsssl_update_option('enable_vulnerability_scanner', 1); // } // } // } /** * Deletes local files if the vulnerability scanner is disabled * * @param $field_id * @param $field_value * @param $prev_value * @param $field_type * * @return void */ public static function maybe_delete_local_files($field_id, $field_value, $prev_value, $field_type): void { if ( $field_id==='enable_vulnerability_scanner' && $field_value !== $prev_value && rsssl_user_can_manage() ) { if ( $field_value == false ) { // Already disabled require_once(rsssl_path . 'security/wordpress/vulnerabilities/class-rsssl-file-storage.php'); \security\wordpress\vulnerabilities\Rsssl_File_Storage::DeleteAll(); } } } public function riskNaming($risk = null) { if (is_null($risk)) { return $this->risk_naming; } return $this->risk_naming[$risk]; } /* Public Section 1: Class Build-up initialization and instancing */ public function run_cron(): void { $this->check_files(); $this->cache_installed_plugins(true); if ( $this->jsons_files_updated ) { if ($this->should_send_mail()) { $this->send_vulnerability_mail(); } $this->check_notice_reset(); } } /** * Check if dismissed notices have to be reset * @return void */ private function check_notice_reset(): void { $this->cache_installed_plugins(); $clear_admin_notices_cache = false; foreach ( $this->risk_levels as $level => $int_level ) { if ( $this->should_reset_notification($level) ) { delete_option("rsssl_" . 'risk_level_' . $level . "_dismissed"); $clear_admin_notices_cache = true; } } if ( $clear_admin_notices_cache ) { RSSSL()->admin->clear_admin_notices_cache(); } } /** * Allow users to manually force a re-check, e.g. in case of manually updating plugins * @return void */ public function force_reload_files(): void { if ( ! rsssl_admin_logged_in() ) { return; } \security\wordpress\vulnerabilities\Rsssl_File_Storage::DeleteOldFiles(); if ( get_option('rsssl_reload_vulnerability_files') ) { delete_option('rsssl_reload_vulnerability_files'); $this->reload_files_on_update(); update_option('rsssl_clear_vulnerability_notices', true, false); set_transient('rsssl_delay_clear', true, 1 * MINUTE_IN_SECONDS ); } if ( get_option('rsssl_clear_vulnerability_notices') && !get_transient('rsssl_delay_clear')) { RSSSL()->admin->clear_admin_notices_cache(); delete_option('rsssl_clear_vulnerability_notices'); } } /** * Checks the files on age and downloads if needed. * @return void */ public function reload_files_on_update(): void { if ( ! rsssl_admin_logged_in() ) { return; } //if the manifest is not older than 4 hours, we don't download it again. if ( $this->get_file_stored_info(false, true) < time() - 14400) { $this->download_manifest(); } $this->download_plugin_vulnerabilities(); $this->download_core_vulnerabilities(); $this->check_notice_reset(); } public function init(): void { if ( ! rsssl_admin_logged_in() ) { return; } //we check the rsssl options if the enable_feedback_in_plugin is set to true if ( rsssl_get_option('enable_feedback_in_plugin') ) { // we enable the feedback in the plugin $this->enable_feedback_in_plugin(); $this->enable_feedback_in_theme(); } //we check if upgrader_process_complete is called, so we can reload the files. add_action('upgrader_process_complete', array($this, 'reload_files_on_update'), 10, 2); add_action('_core_updated_successfully', array($this, 'prepare_reloading_of_files'), 10, 2); //After activation, we need to reload the files. add_action( 'activate_plugin', array($this, 'reload_files_on_update'), 10, 2); //we can also force it add_action( 'admin_init', array($this, 'force_reload_files')); //same goes for themes. add_action('after_switch_theme', array($this, 'reload_files_on_update'), 10, 2); add_action('current_screen', array($this, 'show_inline_code')); } /** * Directly hooking into the core upgrader hook doesn't work, so is too early. * To force this, we save an option we can check later * * @return void */ public function prepare_reloading_of_files(): void { update_option("rsssl_reload_vulnerability_files", true, false); } /** * Function used for first run of the plugin. * * @return array */ public static function firstRun(): array { if ( ! rsssl_user_can_manage() ) { return []; } $self = new self(); $self->check_files(); $self->cache_installed_plugins(true); return [ 'request_success' => true, 'data' => $self->workable_plugins ]; } /** * Get site health notice for vulnerabilities * @return array */ public function get_site_health_notice(): array { if (!rsssl_admin_logged_in()){ return []; } $this->cache_installed_plugins(); $risks = $this->count_risk_levels(); if (count($risks) === 0) { return array( 'label' => __( 'No known vulnerabilities detected', 'really-simple-ssl' ), 'status' => 'good', 'badge' => array( 'label' => __('Security'), 'color' => 'blue', ), 'description' => sprintf( '<p>%s</p>', __( 'No known vulnerabilities detected', 'really-simple-ssl' ) ), 'actions' => '', 'test' => 'health_test', ); } $total = 0; foreach ($this->risk_levels as $risk_level => $value) { $total += $risks[ $risk_level ] ?? 0; } return array( 'label' => __( 'Vulnerabilities detected','really-simple-ssl' ), 'status' => 'critical', 'badge' => array( 'label' => __( 'Security' ), 'color' => 'blue', ), 'description' => sprintf( '<p>%s</p>', sprintf(_n( '%s vulnerability has been detected.', '%s vulnerabilities have been detected.', $total, 'really-simple-ssl' ), number_format_i18n( $total )) . ' '. __( 'Please check the vulnerabilities overview for more information and take appropriate action.' ,'really-simple-ssl' ) ), 'actions' => sprintf( '<p><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></p>', esc_url( rsssl_admin_url([], '#settings/vulnerabilities/vulnerabilities-overview') ), __( 'View vulnerabilities', 'really-simple-ssl' ) ), 'test' => 'rsssl_vulnerabilities', ); } public function show_help_notices($notices) { $this->cache_installed_plugins(); $risks = $this->count_risk_levels(); $level_to_show_on_dashboard = rsssl_get_option('vulnerability_notification_dashboard'); $level_to_show_sitewide = rsssl_get_option('vulnerability_notification_sitewide'); foreach ($this->risk_levels as $risk_level => $value) { if ( !isset($risks[$risk_level]) ) { continue; } //this is shown bases on the config of vulnerability_notification_dashboard $siteWide = false; $dashboardNotice = false; if ( $level_to_show_on_dashboard && $level_to_show_on_dashboard !== '*') { if ($value >= $this->risk_levels[$level_to_show_on_dashboard]) { $dashboardNotice = true; } } if ($level_to_show_sitewide && $level_to_show_sitewide !== '*') { if ($value >= $this->risk_levels[$level_to_show_sitewide]) { $siteWide = true; } } if ( !$dashboardNotice && !$siteWide ) { continue; } $count = $risks[$risk_level]; $title = $this->get_warning_string($risk_level, $count); $notice = [ 'callback' => '_true_', 'score' => 1, 'show_with_options' => ['enable_vulnerability_scanner'], 'output' => [ 'true' => [ 'title' => $title, 'msg' => $title.' '.__('Please take appropriate action.','really-simple-ssl'), 'icon' => ($risk_level === 'c' || $risk_level==='h') ? 'warning' : 'open', 'type' => 'warning', 'dismissible' => true, 'admin_notice' => $siteWide, 'plusone' => true, 'highlight_field_id' => 'vulnerabilities-overview', ] ], ]; $notices['risk_level_' . $risk_level] = $notice; } //now we add the test notices for admin and dahboard. //if the option is filled, we add the test notice. $test_id = get_option('test_vulnerability_tester'); if($test_id) { $dashboard = rsssl_get_option('vulnerability_notification_dashboard'); $side_wide = rsssl_get_option('vulnerability_notification_sitewide'); $site_wide_icon = $side_wide === 'l' || $side_wide === 'm' ? 'open' : 'warning'; if ( $side_wide === 'l' || $side_wide === 'm' || $side_wide === 'h' || $side_wide === 'c') { $notices[ 'test_vulnerability_sitewide_' .$test_id ] = [ 'callback' => '_true_', 'score' => 1, 'show_with_options' => [ 'enable_vulnerability_scanner' ], 'output' => [ 'true' => [ 'title' => __( 'Site wide - Test Notification', 'really-simple-ssl' ), 'msg' => __( 'This is a test notification from Really Simple Security. You can safely dismiss this message.', 'really-simple-ssl' ), 'url' => rsssl_admin_url([], '#settings/vulnerabilities/vulnerabilities-overview'), 'icon' => $site_wide_icon, 'dismissible' => true, 'admin_notice' => true, 'plusone' => true, ] ] ]; } //don't add this one if the same level $dashboard_icon = $dashboard === 'l' || $dashboard === 'm' ? 'open' : 'warning'; if ($dashboard_icon !== $site_wide_icon) { if ( $dashboard === 'l' || $dashboard === 'm' || $dashboard === 'h' || $dashboard === 'c' ) { $notices[ 'test_vulnerability_dashboard_' .$test_id ] = [ 'callback' => '_true_', 'score' => 1, 'show_with_options' => [ 'enable_vulnerability_scanner' ], 'output' => [ 'true' => [ 'title' => __( 'Dashboard - Test Notification', 'really-simple-ssl' ), 'msg' => __( 'This is a test notification from Really Simple Security. You can safely dismiss this message.', 'really-simple-ssl' ), 'icon' => $dashboard_icon, 'dismissible' => true, 'admin_notice' => false, 'plusone' => true, ] ] ]; } } } return $notices; } /** * Generate plugin files for testing purposes. * * @return array */ public static function testGenerator(): array { $mail_notification = rsssl_get_option('vulnerability_notification_email_admin'); if ( $mail_notification === 'l' || $mail_notification === 'm' || $mail_notification === 'h' || $mail_notification === 'c' ) { $mailer = new rsssl_mailer(); $mailer->send_test_mail(); } return []; } /* Public Section 2: DataGathering */ /** * @param $stats * * @return array */ public function get_stats($stats): array { if ( ! rsssl_user_can_manage() ) { return $stats; } $this->cache_installed_plugins(); //now we only get the data we need. $vulnerabilities = array_filter($this->workable_plugins, static function ($plugin) { if (isset($plugin['vulnerable']) && $plugin['vulnerable']) { return $plugin; } return false; }); $time = $this->get_file_stored_info(true); $stats['vulnerabilities'] = count($vulnerabilities); $stats['vulList'] = $vulnerabilities; $riskData = $this->measures_data(); $stats['riskData'] = $riskData['data']; $stats['lastChecked'] = $time; return $stats; } /** * This combines the vulnerabilities with the installed plugins * * And loads it into a memory cache on page load * */ public function cache_installed_plugins($force_update=false): void { if ( ! rsssl_admin_logged_in() ) { return; } if ( !$force_update && !empty($this->workable_plugins) ) { return; } //first we get all installed plugins $installed_plugins = get_plugins(); $installed_themes = wp_get_themes(); //we flatten the array $update = get_site_transient('update_themes'); //we make the installed_themes look like the installed_plugins $installed_themes = array_map( static function ($theme) use ($update) { return [ 'Name' => $theme->get('Name'), 'Slug' => $theme->get('TextDomain'), 'description' => $theme->get('Description'), 'Version' => $theme->get('Version'), 'Author' => $theme->get('Author'), 'AuthorURI' => $theme->get('AuthorURI'), 'PluginURI' => $theme->get('ThemeURI'), 'TextDomain' => $theme->get('TextDomain'), 'RequiresWP' => $theme->get('RequiresWP'), 'RequiresPHP' => $theme->get('RequiresPHP'), 'update_available' => isset($update->response[$theme->get('TextDomain')]), ]; }, $installed_themes); //we add a column type to all values in the array $installed_themes = array_map( static function ($theme) { $theme['type'] = 'theme'; return $theme; }, $installed_themes); //we add a column type to all values in the array //this resets the array keys (currently slugs) so we preserve them in the 'Slug' column. $update = get_site_transient('update_plugins'); $installed_plugins = array_map( static function ($plugin, $slug) use ($update) { $plugin['type'] = 'plugin'; $plugin['update_available'] = isset($update->response[$slug]); $plugin['Slug'] = dirname($slug); $plugin['File'] = $slug; return $plugin; }, $installed_plugins, array_keys($installed_plugins) ); //we merge the two arrays $installed_plugins = array_merge($installed_plugins, $installed_themes); //now we get the components from the file $components = $this->get_components(); //We loop through plugins and check if they are in the components array foreach ($installed_plugins as $plugin) { $slug = $plugin['Slug']; $plugin['vulnerable'] = false; if( $plugin['type'] === 'theme' ) { // we check if the theme exists as a directory $plugin['folder_exists'] = file_exists(get_theme_root() . '/' . $slug ); } if( $plugin['type'] === 'plugin' ) { //also we check if the folder exists for the plugin we added this check for later purposes $plugin['folder_exists'] = file_exists(WP_PLUGIN_DIR . '/' . dirname($slug) ); } //if there are no components, we return if ( !empty($components) ) { foreach ($components as $component) { if ($plugin['Slug'] === $component->slug) { if (!empty($component->vulnerabilities) && $plugin['folder_exists'] === true) { $plugin['vulnerable'] = true; $plugin['risk_level'] = $this->get_highest_vulnerability($component->vulnerabilities); $plugin['rss_identifier'] = $this->getLinkedUUID($component->vulnerabilities, $plugin['risk_level']); $plugin['risk_name'] = $this->risk_naming[$plugin['risk_level']]; $plugin['date'] = $this->getLinkedDate($component->vulnerabilities, $plugin['risk_level']); } } } } //we walk through the components array $this->workable_plugins[$slug] = $plugin; } //now we get the core information $core = $this->get_core(); //we create a plugin like entry for core to add to the workable_plugins array $core_plugin = [ 'Name' => 'WordPress', 'Slug' => 'wordpress', 'Version' => $core->version?? '', 'Author' => 'WordPress', 'AuthorURI' => 'https://wordpress.org/', 'PluginURI' => 'https://wordpress.org/', 'TextDomain' => 'wordpress', 'type' => 'core', ]; $core_plugin['vulnerable'] = false; //we check if there is an update available $update = get_site_transient('update_core'); if (isset($update->updates[0]->response) && $update->updates[0]->response === 'upgrade') { $core_plugin['update_available'] = true; } else { $core_plugin['update_available'] = false; } //if there are no components, we return if ( !empty($core->vulnerabilities) ) { $core_plugin['vulnerable'] = true; $core_plugin['risk_level'] = $this->get_highest_vulnerability($core->vulnerabilities); $core_plugin['rss_identifier'] = $this->getLinkedUUID($core->vulnerabilities, $core_plugin['risk_level']); $core_plugin['risk_name'] = $this->risk_naming[$core_plugin['risk_level']]; $core_plugin['date'] = $this->getLinkedDate($core->vulnerabilities, $core_plugin['risk_level']); } //we add the core plugin to the workable_plugins array $this->workable_plugins['wordpress'] = $core_plugin; } /* Public Section 3: The plugin page add-on */ /** * Callback for the manage_plugins_columns hook to add the vulnerability column * * @param $columns */ public function add_vulnerability_column($columns) { $columns['vulnerability'] = __('Vulnerabilities', 'really-simple-ssl'); return $columns; } /** * Get the data for the risk vulnerabilities table * @param $data * @return array */ public function measures_data(): array { $measures = []; $measures[] = [ 'id' => 'force_update', 'name' => __('Force update', 'really-simple-ssl'), 'value' => get_option('rsssl_force_update'), 'description' => sprintf(__('Will run a frequent update process on vulnerable components.', 'really-simple-ssl'), $this->riskNaming('l')), ]; $measures[] = [ 'id' => 'quarantine', 'name' => __('Quarantine', 'really-simple-ssl'), 'value' => get_option('rsssl_quarantine'), 'description' => sprintf(__('Components will be quarantined if the update process fails.', 'really-simple-ssl'), $this->riskNaming('m')), ]; return [ "request_success" => true, 'data' => $measures ]; } /** * Store the mesures from the api * @param $measures * * @return array */ public function measures_set($measures): array { if (!rsssl_user_can_manage()) { return []; } $risk_data = $measures['riskData'] ?? []; foreach ( $risk_data as $risk ) { if ( !isset($risk['value']) ) { continue; } update_option('rsssl_'.sanitize_title($risk['id']), $this->sanitize_measure($risk['value']), false ); } return []; } /** * Sanitize a measure * * @param string $measure * * @return mixed|string */ public function sanitize_measure($measure) { return isset($this->risk_levels[$measure]) ? $measure : '*'; } /** * Callback for the manage_plugins_custom_column hook to add the vulnerability field * * @param string $column_name * @param string $plugin_file */ public function add_vulnerability_field( string $column_name, string $plugin_file): void { if ( ( $column_name === 'vulnerability' ) ) { $this->cache_installed_plugins(); if ($this->check_vulnerability( $plugin_file ) ) { switch ( $this->check_severity( $plugin_file ) ) { case 'c': printf( '<a class="rsssl-btn-vulnerable rsssl-critical" target="_blank" rel="noopener noreferrer" href="%s">%s</a>', rsssl_link('vulnerability/' . $this->getIdentifier( $plugin_file ) ), ucfirst( $this->risk_naming['c'] ) ); break; case 'h': printf( '<a class="rsssl-btn-vulnerable rsssl-high" target="_blank" rel="noopener noreferrer" href="%s">%s</a>', rsssl_link('vulnerability/' . $this->getIdentifier( $plugin_file ) ), ucfirst( $this->risk_naming['h'] ) ); break; case 'm': printf( '<a class="rsssl-btn-vulnerable rsssl-medium" target="_blank" rel="noopener noreferrer" href="%s">%s</a>', rsssl_link('vulnerability/' . $this->getIdentifier( $plugin_file ) ), ucfirst( $this->risk_naming['m'] ) ); break; default: echo sprintf( '<a class="rsssl-btn-vulnerable rsssl-low" target="_blank" rel="noopener noreferrer" href="%s">%s</a>', rsssl_link('vulnerability/' . $this->getIdentifier( $plugin_file ) ), ucfirst( $this->risk_naming['l'] ) ); break; } } if ( $this->is_quarantined($plugin_file)) { echo sprintf( '<a class="rsssl-btn-vulnerable rsssl-critical" target="_blank" rel="noopener noreferrer" href="%s">%s</a>', 'https://really-simple-ssl.com/instructions/about-vulnerabilities/#quarantine' , __("Quarantined","really-simple-ssl") ); } } } /** * Callback for the admin_enqueue_scripts hook to add the vulnerability styles * * @param $hook * * @return void */ public function add_vulnerability_styles($hook) { if ('plugins.php' !== $hook) { return; } //only on settings page $rtl = is_rtl() ? 'rtl/' : ''; $url = trailingslashit(rsssl_url) . "assets/css/{$rtl}rsssl-plugin.min.css"; $path = trailingslashit(rsssl_path) . "assets/css/{$rtl}rsssl-plugin.min.css"; if (file_exists($path)) { wp_enqueue_style('rsssl-plugin', $url, array(), rsssl_version); } } /** * checks if the plugin is vulnerable * * @param $plugin_file * * @return mixed */ private function check_vulnerability($plugin_file) { return $this->workable_plugins[ dirname($plugin_file) ]['vulnerable'] ?? false; } /** * Check if a plugin is quarantined * * @param string $plugin_file * * @return bool */ private function is_quarantined(string $plugin_file): bool { return strpos($plugin_file, '-rsssl-q-')!==false; } /** * checks if the plugin's severity closed * * @param $plugin_file * * @return mixed */ private function check_severity($plugin_file) { return $this->workable_plugins[dirname($plugin_file)]['risk_level']; } private function getIdentifier($plugin_file) { return $this->workable_plugins[dirname($plugin_file)]['rss_identifier']; } /* End of plug-in page add-on */ /* Public and private functions | Files and storage */ /** * Checks the files on age and downloads if needed. * * @return void */ public function check_files(): void { if ( ! rsssl_admin_logged_in() ) { return; } //we download the manifest file if it doesn't exist or is older than 12 hours if ($this->validate_local_file(false, true)) { if ( $this->get_file_stored_info(false, true) < time() - $this->interval ) { $this->download_manifest(); } } else { $this->download_manifest(); } //We check the core vulnerabilities and validate age and existence if ($this->validate_local_file(true, false)) { //if the file is younger than 12 hours, we don't download it again. if ($this->get_file_stored_info(true) < time() - $this->interval ) { $this->download_core_vulnerabilities(); } } else { $this->download_core_vulnerabilities(); } //We check the plugin vulnerabilities and validate age and existence if ($this->validate_local_file()) { if ($this->get_file_stored_info() < time() - $this->interval ) { $this->download_plugin_vulnerabilities(); } } else { $this->download_plugin_vulnerabilities(); } } /** * Checks if the file is valid and exists. It checks three files: the manifest, the core vulnerabilities and the plugin vulnerabilities. * * @param bool $isCore * @param bool $manifest * * @return bool */ private function validate_local_file(bool $isCore = false, bool $manifest = false): bool { if ( ! rsssl_admin_logged_in() ) { return false; } if (!$manifest) { //if we don't check for the manifest, we check the other files. $isCore ? $file = 'core.json' : $file = 'components.json'; } else { $file = 'manifest.json'; } $upload_dir = Rsssl_File_Storage::get_upload_dir(); $file = $upload_dir . '/' . $file; if (file_exists($file)) { //now we check if the file is older than 3 days, if so, we download it again $file_time = filemtime($file); $now = time(); $diff = $now - $file_time; $days = floor($diff / (60 * 60 * 24)); if ($days < 1) { return true; } } return false; } /** * Downloads bases on given url * * @param string $url * * @return mixed|null */ private function download(string $url) { if ( ! rsssl_admin_logged_in() ) { return null; } //now we check if the file remotely exists and then log an error if it does not. $response = wp_remote_get( $url ); if ( is_wp_error( $response ) ) { return null; } if ( wp_remote_retrieve_response_code($response) !== 200 ) { return null; } $json = wp_remote_retrieve_body($response); return json_decode($json); } private function remote_file_exists($url): bool { try { $headers = @get_headers($url); if ($headers === false) { // URL is not accessible or some error occurred return false; } // Check if the HTTP status code starts with "200" (indicating success) return strpos($headers[0], '200') !== false; // Rest of your code handling $headers goes here } catch (Exception $e) { return false; } } /** * Stores a full core or component file in the upload folder * * @param $data * @param bool $isCore * @param bool $manifest * * @return void */ private function store_file($data, bool $isCore = false, bool $manifest = false): void { if ( ! rsssl_admin_logged_in() ) { return; } //we get the upload directory $upload_dir = Rsssl_File_Storage::get_upload_dir(); if ( !$manifest ) { $file = $upload_dir . '/' . ($isCore ? 'core.json' : 'components.json'); } else { $file = $upload_dir . '/manifest.json'; } //we delete the old file if it exists if ( file_exists($file) ) { wp_delete_file($file); } //if the data is empty, we return null if ( empty($data) ) { return; } Rsssl_File_Storage::StoreFile($file, $data); $this->jsons_files_updated = true; } public function get_file_stored_info($isCore = false, $manifest = false) { if ( ! rsssl_admin_logged_in() ) { return false; } $upload_dir = Rsssl_File_Storage::get_upload_dir(); if ($manifest) { $file = $upload_dir . '/manifest.json'; if (!file_exists($file)) { return false; } return Rsssl_File_Storage::GetDate($file); } $file = $upload_dir . '/' . ($isCore ? 'core.json' : 'components.json'); if (!file_exists($file)) { return false; } return Rsssl_File_Storage::GetDate($file); } /* End of files and Storage */ /* Section for the core files Note: No manifest is needed */ /** * Downloads the vulnerabilities for the current core version. * * @return void */ protected function download_core_vulnerabilities(): void { if ( ! rsssl_admin_logged_in() ) { return; } global $wp_version; $url = self::RSSSL_SECURITY_API . 'core/WordPress.json'; $data = $this->download($url); if (!$data) { return; } $data->vulnerabilities = $this->filter_vulnerabilities($data->vulnerabilities, $wp_version, true); //first we store this as a json file in the uploads folder $this->store_file($data, true); } /* End of core files section */ /* Section for the plug-in files */ /** * Downloads the vulnerabilities for the current plugins. * * @return void */ protected function download_plugin_vulnerabilities(): void { if ( ! rsssl_admin_logged_in() ) { return; } //we get all the installed plugins $installed_plugins = get_plugins(); //first we get the manifest file $manifest = $this->getManifest(); $vulnerabilities = []; foreach ($installed_plugins as $file => $plugin) { $slug = dirname($file); $installed_plugins[ $file ]['Slug'] = $slug; $url = self::RSSSL_SECURITY_API . 'plugin/' . $slug . '.json'; //if the plugin is not in the manifest, we skip it if (!in_array($slug, (array)$manifest)) { continue; } $data = $this->download($url); if ($data !== null) { $vulnerabilities[] = $data; } } //we also do it for all the installed themes $installed_themes = wp_get_themes(); foreach ($installed_themes as $theme) { $theme = $theme->get('TextDomain'); $url = self::RSSSL_SECURITY_API . 'theme/' . $theme . '.json'; //if the plugin is not in the manifest, we skip it if (!in_array($theme, (array)$manifest)) { continue; } $data = $this->download($url); if ($data !== null) { $vulnerabilities[] = $data; } } //we make the installed_themes look like the installed_plugins $installed_themes = array_map( static function ($theme) { return [ 'Name' => $theme->get('Name'), 'Slug' => $theme->get('TextDomain'), 'description' => $theme->get('Description'), 'Version' => $theme->get('Version'), 'Author' => $theme->get('Author'), 'AuthorURI' => $theme->get('AuthorURI'), 'PluginURI' => $theme->get('ThemeURI'), 'TextDomain' => $theme->get('TextDomain'), 'RequiresWP' => $theme->get('RequiresWP'), 'RequiresPHP' => $theme->get('RequiresPHP'), ]; }, $installed_themes); //we merge $installed_plugins and $installed_themes $installed_plugins = array_merge($installed_plugins, $installed_themes); //we filter the vulnerabilities $vulnerabilities = $this->filter_active_components($vulnerabilities, $installed_plugins); $this->store_file($vulnerabilities); } /** * Loads the info from the files Note this is also being used for the themes. * * @return mixed|null */ private function get_components() { if ( ! rsssl_admin_logged_in() ) { return []; } $upload_dir = Rsssl_File_Storage::get_upload_dir(); $file = $upload_dir . '/components.json'; if (!file_exists($file)) { return []; } $components = Rsssl_File_Storage::GetFile($file); if (!is_array($components)) $components = []; return $components; } /* End of plug-in files section */ /* Section for the core files Note: No manifest is needed */ private function get_core() { if ( ! rsssl_admin_logged_in() ) { return null; } $upload_dir = Rsssl_File_Storage::get_upload_dir(); $file = $upload_dir . '/core.json'; if (!file_exists($file)) { return false; } return Rsssl_File_Storage::GetFile($file); } /* Section for the theme files */ public function enable_feedback_in_theme(): void { //Logic here for theme warning Create Callback and functions for these steps //we only display the warning for the theme page add_action('current_screen', [$this, 'show_theme_warning']); } public function show_theme_warning($hook) { $screen = get_current_screen(); if ($screen && $screen->id !== 'themes') { return; } //we add warning scripts to themes add_action('admin_enqueue_scripts', [$this, 'enqueue_theme_warning_scripts']); } public function show_inline_code($hook): void { if ($hook !== 'themes.php') { return; } //we add warning scripts to themes add_action('admin_footer', [$this, 'enqueue_theme_warning_scripts']); } public function enqueue_theme_warning_scripts(): void { //we get all components with vulnerabilities $components = $this->get_components(); ob_start();?> <script> window.addEventListener("load", () => { let style = document.createElement('style'); let vulnerable_components = [<?php echo implode(',', array_map(function ($component) { return "{slug: '" . esc_attr($component->slug) . "', risk: '" . esc_attr($this->get_highest_vulnerability($component->vulnerabilities)) . "'}"; }, $components)) ?>]; //we create the style for warning style.innerHTML = '.rsssl-theme-notice {box-shadow: 0 1px 1px 0 rgba(0,0,0,.1); position:relative; z-index:50; margin-bottom: -35px; padding: 8px 12px;}'; style.innerHTML += '.rsssl-theme-notice-warning {background-color: #FFF6CE; border-left: 4px solid #ffb900;}'; //we create the style for danger style.innerHTML += '.rsssl-theme-notice-danger {background-color: #FFCECE; border-left: 4px solid #dc3232;}'; style.innerHTML += '.rsssl-theme-notice-below-notice{margin-top: 41px;}'; style.innerHTML += '.rsssl-theme-notice-warning .dashicons, .rsssl-theme-notice-danger .dashicons{margin-right: 12px;}'; let levels = <?php echo json_encode($this->risk_naming)?>; //we add the style to the head document.head.appendChild(style); //we loop through the components vulnerable_components.forEach(function(component) { //we get the theme element let theme_element = document.querySelector(".theme[data-slug='"+component.slug+"']"); //if the theme exists if (theme_element) { //check if theme element contains notice. if so, push this notice down with class rsssl-theme-notice-below-notice let hasNotice = theme_element.querySelector('.update-message.notice'); //we check the risk let level = levels[component.risk]; let text = '<?php echo esc_attr(__('Vulnerability: %s', 'really-simple-ssl')) ?>'; text = text.replace('%s', level); let divClass = ' rsssl-theme-notice '; divClass += component.risk === 'h' || component.risk === 'c' ? 'rsssl-theme-notice-danger' : 'rsssl-theme-notice-warning'; if (hasNotice) divClass += ' rsssl-theme-notice-below-notice'; theme_element.insertAdjacentHTML('afterbegin', ` <div class="${divClass}"> <div><span class="dashicons dashicons-info"></span>${text}</div> </div> `); } }); //find quarantined themes, find all themes where the data-slug contains '-rsssl-q' document.querySelectorAll(".theme[data-slug*='-rsssl-q']").forEach(function(theme_element) { //if the theme exists if ( theme_element ) { //we check the risk let text = '<?php echo esc_attr(__('Quarantined', 'really-simple-ssl')) ?>'; let divClass = 'rsssl-theme-notice rsssl-theme-notice-danger'; theme_element.insertAdjacentHTML('afterbegin', ` <div class="${divClass}"> <div><span class="dashicons dashicons-info"></span> <a href="https://really-simple-ssl.com/instructions/about-vulnerabilities/#quarantine" target="_blank" rel="noopener noreferrer">${text}</a> </div> </div> `); } }); }); </script> <?php echo ob_get_clean(); } /* End of theme files section */ /* Private functions | Filtering and walks */ /** * Filters the components based on the active plugins * * @param $components * @param array $active_plugins * * @return array */ private function filter_active_components($components, array $active_plugins): array { $active_components = []; foreach ($components as $component) { foreach ($active_plugins as $active_plugin) { if (isset($component->slug) && $component->slug === $active_plugin['Slug']) { //now we filter out the relevant vulnerabilities $component->vulnerabilities = $this->filter_vulnerabilities($component->vulnerabilities, $active_plugin['Version']); //if we have vulnerabilities, we add the component to the active components or when the plugin is closed if (count($component->vulnerabilities) > 0 || $component->status === 'closed') { $active_components[] = $component; } } } } return $active_components; } /** * This function adds the vulnerability with the highest risk to the plugins page * * @param $vulnerabilities * * @return string */ private function get_highest_vulnerability($vulnerabilities): string { //we loop through the vulnerabilities and get the highest risk level $highest_risk_level = 0; foreach ($vulnerabilities as $vulnerability) { if ($vulnerability->severity === null) { continue; } if (!isset($this->risk_levels[$vulnerability->severity])) { continue; } if ($this->risk_levels[$vulnerability->severity] > $highest_risk_level) { $highest_risk_level = $this->risk_levels[$vulnerability->severity]; } } //we now loop through the risk levels and return the highest one foreach ($this->risk_levels as $key => $value) { if ($value === $highest_risk_level) { return $key; } } return 'l'; } /* End of private functions | Filtering and walks */ /* Private functions | End of Filtering and walks */ /* Private functions | Feedback, Styles and scripts */ /** * This function shows the feedback in the plugin * * @return void */ private function enable_feedback_in_plugin(): void { //we add some styling to this page add_action('admin_enqueue_scripts', array($this, 'add_vulnerability_styles')); //we add an extra column to the plugins page add_filter('manage_plugins_columns', array($this, 'add_vulnerability_column')); add_filter('manage_plugins-network_columns', array($this, 'add_vulnerability_column')); //now we add the field to the plugins page add_action('manage_plugins_custom_column', array($this, 'add_vulnerability_field'), 10, 3); add_action('manage_plugins-network_custom_column', array($this, 'add_vulnerability_field'), 10, 3); } /* End of private functions | Feedback, Styles and scripts */ /** * This function downloads the manifest file from the api server * * @return void */ private function download_manifest(): void { if ( ! rsssl_admin_logged_in() ) { return; } $url = self::RSSSL_SECURITY_API . 'manifest.json'; $data = $this->download($url); //we convert the data to an array $data = json_decode(json_encode($data), true); //first we store this as a json file in the uploads folder $this->store_file($data, true, true); } /** * This function downloads the created file from the uploads * * @return false|void */ private function getManifest() { if ( ! rsssl_admin_logged_in() ) { return false; } $upload_dir = Rsssl_File_Storage::get_upload_dir(); $file = $upload_dir . '/manifest.json'; if (!file_exists($file)) { return false; } return Rsssl_File_Storage::GetFile($file); } private function filter_vulnerabilities($vulnerabilities, $Version, $core = false): array { $filtered_vulnerabilities = array(); foreach ( $vulnerabilities as $vulnerability ) { //if fixed_in contains a version, and the current version is higher than the fixed_in version, we skip it as fixed. //This needs to be a positive check only, as the fixed_in value is less accurate than the version_from and version_to values if ( function_exists( 'rsssl_version_compare' ) ) { if ( $vulnerability->fixed_in !== 'not fixed' && rsssl_version_compare( $Version, $vulnerability->fixed_in, '>=' ) ) { continue; } } else { # fallback if ( $vulnerability->fixed_in !== 'not fixed' && version_compare( $Version, $vulnerability->fixed_in, '>=' ) ) { continue; } } //we have the fields version_from and version_to and their needed operators $version_from = $vulnerability->version_from; $version_to = $vulnerability->version_to; $operator_from = $vulnerability->operator_from; $operator_to = $vulnerability->operator_to; //we now check if the version is between the two versions if ( function_exists( 'rsssl_version_compare' ) ) { if ( rsssl_version_compare( $Version, $version_from, $operator_from ) && rsssl_version_compare( $Version, $version_to, $operator_to ) ) { $filtered_vulnerabilities[] = $vulnerability; } } else { if ( version_compare( $Version, $version_from, $operator_from ) && version_compare( $Version, $version_to, $operator_to ) ) { $filtered_vulnerabilities[] = $vulnerability; } } } return $filtered_vulnerabilities; } /** * Get count of risk occurrence for each risk level * @return array */ public function count_risk_levels(): array { $plugins = $this->workable_plugins; $risk_levels = array(); foreach ($plugins as $plugin) { if (isset($plugin['risk_level'])) { if (isset($risk_levels[$plugin['risk_level']])) { $risk_levels[$plugin['risk_level']]++; } else { $risk_levels[$plugin['risk_level']] = 1; } } } return $risk_levels; } /** * check if a a dismissed notice should be reset * * @param string $risk_level * * @return bool */ private function should_reset_notification(string $risk_level): bool { $plugins = $this->workable_plugins; $vulnerable_plugins = array(); foreach ($plugins as $plugin) { if (isset($plugin['risk_level']) && $plugin['risk_level'] === $risk_level) { $vulnerable_plugins[] = $plugin['rss_identifier']; } } $dismissed_for = get_option("rsssl_{$risk_level}_notification_dismissed_for",[]); //cleanup. Check if plugins in mail_sent_for exist in the $plugins array foreach ($dismissed_for as $key => $rss_identifier) { if ( ! in_array($rss_identifier, $vulnerable_plugins) ) { unset($dismissed_for[$key]); } } $diff = array_diff($vulnerable_plugins, $dismissed_for); foreach ($diff as $rss_identifier) { if (!in_array($rss_identifier, $dismissed_for)){ $dismissed_for[] = $rss_identifier; } } //add the new plugins to the $dismissed_for array update_option("rsssl_{$risk_level}_notification_dismissed_for", $dismissed_for, false ); return !empty($diff); } /** * check if a new mail should be sent about vulnerabilities * @return bool */ private function should_send_mail(): bool { $plugins = $this->workable_plugins; $vulnerable_plugins = array(); foreach ($plugins as $plugin) { if (isset($plugin['risk_level'])) { $vulnerable_plugins[] = $plugin['rss_identifier']; } } $mail_sent_for = get_option('rsssl_vulnerability_mail_sent_for',[]); //cleanup. Check if plugins in mail_sent_for exist in the $plugins array foreach ($mail_sent_for as $key => $rss_identifier) { if ( ! in_array($rss_identifier, $vulnerable_plugins) ) { unset($mail_sent_for[$key]); } } $diff = array_diff($vulnerable_plugins, $mail_sent_for); foreach ($diff as $rss_identifier) { if (!in_array($rss_identifier, $mail_sent_for)){ $mail_sent_for[] = $rss_identifier; } } //add the new plugins to the mail_sent_for array update_option('rsssl_vulnerability_mail_sent_for',$mail_sent_for, false ); return !empty($diff); } /** * Get id by risk level * @param array $vulnerabilities * @param string $risk_level * * @return mixed|void */ private function getLinkedUUID( array $vulnerabilities, string $risk_level) { foreach ($vulnerabilities as $vulnerability) { if ($vulnerability->severity === $risk_level) { return $vulnerability->rss_identifier; } } } private function getLinkedDate($vulnerabilities, string $risk_level) { foreach ($vulnerabilities as $vulnerability) { if ($vulnerability->severity === $risk_level) { //we return the date in a readable format return date(get_option('date_format'), strtotime($vulnerability->published_date)); } } } /** * Send email warning * @return void */ public function send_vulnerability_mail(): void { if ( ! rsssl_admin_logged_in() ) { return; } //first we check if the user wants to receive emails if ( !rsssl_get_option('send_notifications_email') ) { return; } $level_for_email = rsssl_get_option('vulnerability_notification_email_admin'); if ( !$level_for_email || $level_for_email === '*' ) { return; } //now based on the risk level we send a different email $risk_levels = $this->count_risk_levels(); $total = 0; $blocks = []; foreach ($risk_levels as $risk_level => $count) { if ( $this->risk_levels[$risk_level] >= $this->risk_levels[$level_for_email] ) { $blocks[] = $this->createBlock($risk_level, $count); $total += $count; } } //date format is named month day year $mailer = new rsssl_mailer(); $mailer->subject = sprintf(__("Vulnerability Alert: %s", "really-simple-ssl"), $this->site_url() ); $mailer->title = sprintf(_n("%s: %s vulnerability found", "%s: %s vulnerabilities found", $total, "really-simple-ssl"), $this->date(), $total); $message = sprintf(__("This is a vulnerability alert from Really Simple Security for %s. ","really-simple-ssl"), $this->domain() ); $mailer->message = $message; $mailer->warning_blocks = $blocks; if ($total > 0) { //if for some reason the total is 0, we don't send an email $mailer->send_mail(); } } /** * Create an email block by risk level * * @param string $risk_level * @param int $count * * @return array */ protected function createBlock(string $risk_level, int $count): array { $plugin_name = ''; //if we have only one plugin with this risk level, we can show the plugin name //we search it in the list if ( $count===1 ){ $plugins = $this->workable_plugins; foreach ($plugins as $plugin) { if (isset($plugin['risk_level']) && $plugin['risk_level'] === $risk_level) { $plugin_name = $plugin['Name']; } } } $risk = $this->risk_naming[$risk_level]; $title = $this->get_warning_string($risk_level, $count); $message = $count === 1 ? sprintf(__("A %s vulnerability is found in %s.", "really-simple-ssl"),$risk, $plugin_name) : sprintf(__("Multiple %s vulnerabilities have been found.", "really-simple-ssl"), $risk); return [ 'title' => $title, 'message' => $message . ' ' . __('Based on your settings, Really Simple Security will take appropriate action, or you will need to solve it manually.','really-simple-ssl') .' '. sprintf(__('Get more information from the Really Simple Security dashboard on %s'), $this->domain() ), 'url' => rsssl_admin_url( [], '#settings/vulnerabilities/vulnerabilities-overview'), ]; } /** * @param string $risk_level * @param int $count * * @return string */ public function get_warning_string( string $risk_level, int $count): string { switch ($risk_level){ case 'c': $warning = sprintf(_n('You have %s critical vulnerability', 'You have %s critical vulnerabilities', $count, 'really-simple-ssl'), $count); break; case 'h': $warning = sprintf(_n('You have %s high-risk vulnerability', 'You have %s high-risk vulnerabilities', $count, 'really-simple-ssl'), $count); break; case 'm': $warning = sprintf(_n('You have %s medium-risk vulnerability', 'You have %s medium-risk vulnerabilities', $count, 'really-simple-ssl'), $count); break; default: $warning = sprintf(_n('You have %s low-risk vulnerability', 'You have %s low-risk vulnerabilities', $count, 'really-simple-ssl'), $count); break; } return $warning; } /** * Get a nicely formatted date for today's date * * @return string */ public function date(): string { return date_i18n( get_option( 'date_format' )); } /** * Get the domain name in a clickable format * * @return string */ public function domain(): string { return '<a href="'.$this->site_url().'" target="_blank" rel="noopener noreferrer">'.$this->site_url().'</a>'; } /** * Cron triggers may sometimes result in http URL's, even though SSL is enabled in Really Simple Security. * We ensure that the URL is returned with https if SSL is enabled. * * @return string */ public function site_url(): string { $ssl_enabled = rsssl_get_option('ssl_enabled') || is_ssl(); $scheme = $ssl_enabled ? 'https' : 'http'; return get_site_url(null, '', $scheme); } } //we initialize the class //add_action('init', array(rsssl_vulnerabilities::class, 'instance')); if ( !defined('rsssl_pro') ) { $vulnerabilities = new rsssl_vulnerabilities(); } } ######################################################################################### # Functions for the vulnerability scanner # # These functions are used in the vulnerability scanner like the notices and the api's # ######################################################################################### //we clear all the cache when the vulnerability scanner is enabled function rsssl_vulnerabilities_api( array $response, string $action, $data ): array { if ( ! rsssl_user_can_manage() ) { return $response; } switch ($action) { case 'vulnerabilities_test_notification': //creating a random string based on time. $random_string = md5( time() ); update_option( 'test_vulnerability_tester', $random_string, false ); //clear admin notices cache delete_option('rsssl_admin_notices'); $response = rsssl_vulnerabilities::testGenerator(); break; case 'vulnerabilities_scan_files': $response = rsssl_vulnerabilities::firstRun(); break; case 'vulnerabilities_measures_get': $response = ( new rsssl_vulnerabilities )->measures_data(); break; case 'vulnerabilities_measures_set': $response = ( new rsssl_vulnerabilities )->measures_set($data); break; } return $response; } add_filter( 'rsssl_do_action', 'rsssl_vulnerabilities_api', 10, 3 ); /* End of Routing and API's */ wordpress/vulnerabilities/class-rsssl-folder-name.php 0000777 00000003206 15251751331 0017161 0 ustar 00 <?php namespace security\wordpress\vulnerabilities; require_once rsssl_path . '/lib/admin/class-helper.php'; use RSSSL\lib\admin\Helper; class Rsssl_Folder_Name { use Helper; public $folderName; private function __construct() { $this->initializeFolderName(); $this->verifyAndCreateFolder(); } private function initializeFolderName(): void { $rsssl_folder = get_option( 'rsssl_folder_name' ); if ( $rsssl_folder ) { $this->folderName = $this->folderName( $rsssl_folder ); } else { $newFolderName = 'really-simple-ssl/' . md5( uniqid( mt_rand(), true ) ); $this->folderName = $this->folderName( $newFolderName ); require_once 'class-rsssl-file-storage.php'; Rsssl_File_Storage::DeleteOldFiles(); update_option( 'rsssl_folder_name', $this->folderName ); } } private function folderName( $name ): string { return $name; } private function verifyAndCreateFolder(): void { $upload_dir = wp_upload_dir(); if ( ! file_exists( $upload_dir['basedir'] . '/' . $this->folderName ) ) { $this->createFolder(); } } public function createFolder(): void { $upload_dir = wp_upload_dir(); $folder_path = $upload_dir['basedir'] . '/' . $this->folderName; if ( ! file_exists( $folder_path ) && is_writable($upload_dir['basedir'] ) ) { if ( ! mkdir( $folder_path, 0755, true ) && ! is_dir( $folder_path ) ) { $this->log( sprintf( 'Really Simple Security: Directory "%s" was not created', $folder_path ) ); } } } /** * Creates a new folder name and saves it in the settings * * @return string */ public static function getFolderName(): string { return (new Rsssl_Folder_Name())->folderName; } } wordpress/vulnerabilities/FileStorage.php 0000777 00000005704 15251751331 0014732 0 ustar 00 <?php namespace security\wordpress\vulnerabilities; defined('ABSPATH') or die(); class FileStorage { private $hash; /** * FileStorage constructor. */ public function __construct() { //Fetching the key from the database $this->generateHashKey(); } public Static function StoreFile($file, $data) { $storage = new FileStorage(); $storage->set($data, $file); } public Static function GetFile($file) { $storage = new FileStorage(); return $storage->get($file); } /** Get the data from the file * @param $file * @return bool|mixed */ public function get($file) { if (file_exists($file)) { $data = file_get_contents($file); $data = $this->Decode64WithHash($data); return json_decode($data); } return false; } /** Save the data to the file * @param $data * @param $file */ public function set($data, $file) { $data = $this->Encode64WithHash(json_encode($data)); file_put_contents($file, $data); } /** encode the data with a hash * @param $data * @return string */ private function Encode64WithHash($data): string { //we create a simple encoding, using the hashkey as a salt $data = base64_encode($data); return base64_encode($data . $this->hash); } /** decode the data with a hash * @param $data * @return string */ private function Decode64WithHash($data): string { //we create a simple decoding, using the hashkey as a salt $data = base64_decode($data); $data = substr($data, 0, -strlen($this->hash)); return base64_decode($data); } /** Generate a hashkey and store it in the database * @return void */ private function generateHashKey(): void { if (get_option('rsssl_hashkey') && get_option('rsssl_hashkey') !== "") { $this->hash = get_option('rsssl_hashkey'); } else { $this->hash = md5(uniqid(rand(), true)); update_option('rsssl_hashkey', $this->hash, false); } } public static function GetDate(string $file) { if (file_exists($file)) { return filemtime($file); } return false; } public static function DeleteAll() { //we get the upload folder $upload_dir = wp_upload_dir(); //we get the really-simple-ssl folder $rsssl_dir = $upload_dir['basedir'] . '/really-simple-ssl'; //then we delete the following files from that folder: manifest.json, components.json and core.json $files = array('manifest.json', 'components.json', 'core.json'); foreach ($files as $file) { //we delete the file $file = $rsssl_dir . '/' . $file; if (file_exists($file)) { unlink($file); } } } } wordpress/vulnerabilities/class-rsssl-file-storage.php 0000777 00000010614 15251751331 0017352 0 ustar 00 <?php namespace security\wordpress\vulnerabilities; defined( 'ABSPATH' ) or die(); require_once rsssl_path . 'lib/admin/class-encryption.php'; require_once 'class-rsssl-folder-name.php'; use RSSSL\lib\admin\Encryption; class Rsssl_File_Storage { use Encryption; public $folder; //for the folder name /** * Rsssl_File_Storage constructor. */ public function __construct() { //Fetching the key from the database $upload_dir = wp_upload_dir(); $this->folder = $upload_dir['basedir'] . '/' . Rsssl_Folder_Name::getFolderName(); } public static function StoreFile( $file, $data ): void { $storage = new Rsssl_File_Storage(); //first we check if the storage folder is already in the $file string if ( strpos( $file, $storage->folder ) !== false ) { $file = str_replace( $storage->folder . '/', '', $file ); } $storage->set( $data, $storage->folder . '/' . $file ); } public static function GetFile( $file ) { $storage = new Rsssl_File_Storage(); //first we check if the storage folder is already in the $file string if ( strpos( $file, $storage->folder ) !== false ) { $file = str_replace( $storage->folder . '/', '', $file ); } return $storage->get( $storage->folder . '/' . $file ); } /** Get the data from the file * * @param $file * * @return bool|mixed */ public function get( $file ) { if ( file_exists( $file ) ) { $data = file_get_contents( $file ); $data = $this->decrypt( $data ); return json_decode( $data ); } return false; } /** Save the data to the file * * @param $data * @param $file */ public function set( $data, $file ) { if ( ! is_dir( $this->folder ) ) { return; } if ( ! is_writable( $this->folder ) ) { return; } $data = $this->encrypt( json_encode( $data ) ); //first we check if the storage folder is already in the $file string if ( strpos( $file, $this->folder ) !== false ) { $file = str_replace( $this->folder . '/', '', $file ); } file_put_contents( $this->folder . '/' . $file, $data ); } public static function GetDate( string $file ) { if ( file_exists( $file ) ) { return filemtime( $file ); } return false; } public static function get_upload_dir() { return ( new Rsssl_File_Storage() )->folder; } public static function validateFile( string $file ): bool { $storage = new Rsssl_File_Storage(); $file = $storage->folder . '/' . $file; if ( file_exists( $file ) ) { return true; } return false; } /** * Delete all files in the storage folder * * @return void */ public static function DeleteAll(): void { $storage = new Rsssl_File_Storage(); //we get the really-simple-ssl folder $rsssl_dir = $storage->folder; //then we delete the following files from that folder: manifest.json, components.json and core.json $files = array( 'manifest.json', 'components.json', 'core.json' ); foreach ( $files as $file ) { //we delete the file $file = $rsssl_dir . '/' . $file; if ( file_exists( $file ) ) { unlink( $file ); } } //we delete the folder if ( file_exists( $rsssl_dir ) ) { self::DeleteFolder($rsssl_dir); //we delete the option delete_option( 'rsssl_folder_name' ); } } /** * Recursively delete a folder and its contents. * * @param string $dir The path to the folder to be deleted. * * @return bool Returns true if the folder was successfully deleted, false otherwise. */ public static function DeleteFolder($dir): bool { if (substr($dir, strlen($dir) - 1, 1) != '/') $dir .= '/'; if ($handle = opendir($dir)) { while ($obj = readdir($handle)) { if ($obj != '.' && $obj != '..') { if (is_dir($dir.$obj)) { if (!self::DeleteFolder($dir.$obj)) return false; } elseif (is_file($dir.$obj)) { if (!unlink($dir.$obj)) return false; } } } closedir($handle); if (!rmdir($dir)) return false; return true; } return false; } /** * Delete all files in the storage folder * * @return void */ public static function DeleteOldFiles(): void { $rsssl_dir = wp_upload_dir()['basedir'] . '/really-simple-ssl'; //then we delete the following files from that folder: manifest.json, components.json and core.json $files = array( 'manifest.json', 'components.json', 'core.json' ); foreach ( $files as $file ) { //we delete the file $file = $rsssl_dir . '/' . $file; if ( file_exists( $file ) ) { unlink( $file ); } } } } wordpress/rename-admin-user.php 0000777 00000016442 15251751331 0012637 0 ustar 00 <?php defined('ABSPATH') or die(); /** * Username 'admin' changed notice * @return array */ function rsssl_admin_username_changed( $notices ) { $notices['username_admin_changed'] = array( 'condition' => ['rsssl_username_admin_changed'], 'callback' => '_true_', 'score' => 5, 'output' => array( 'true' => array( 'msg' => sprintf(__("Username 'admin' has been changed to %s", "really-simple-ssl"),esc_html(get_site_transient('rsssl_username_admin_changed')) ), 'icon' => 'open', 'dismissible' => true, ), ), ); return $notices; } add_filter('rsssl_notices', 'rsssl_admin_username_changed'); /** * Add admin as not allowed username * @param array $illegal_user_logins * * @return array */ function rsssl_prevent_admin_user_add(array $illegal_user_logins){ $illegal_user_logins[] = 'admin'; $illegal_user_logins[] = 'administrator'; return $illegal_user_logins; } add_filter( 'illegal_user_logins', 'rsssl_prevent_admin_user_add' ); /** * Rename admin user * @return bool */ function rsssl_rename_admin_user() { if ( !rsssl_user_can_manage() ) { return false; } //to be able to update the admin user email, we need to disable this filter temporarily remove_filter( 'illegal_user_logins', 'rsssl_prevent_admin_user_add' ); // Get user data for login admin $admin_user = get_user_by('login','admin'); if ( $admin_user ) { // Get the new user login $new_user_login = trim(sanitize_user(rsssl_get_option('new_admin_user_login'))); if ( rsssl_new_username_valid() ) { $admin_user_id = $admin_user->data->ID; $admin_userdata = get_userdata( $admin_user_id ); $admin_email = $admin_userdata->data->user_email; global $wpdb; //get current user hash $user_hash = $wpdb->get_var($wpdb->prepare("select user_pass from {$wpdb->base_prefix}users where ID = %s", $admin_user_id) ); //create temp email address $domain = site_url(); $parse = parse_url( $domain ); $host = $parse['host'] ?? 'example.com'; $email = "$new_user_login@$host"; // Do not send an e-mail with this temporary e-mail address add_filter('send_email_change_email', '__return_false'); // update e-mail for existing user. Cannot have two accounts connected to the same e-mail address $success = wp_update_user( array( 'ID' => $admin_user_id, 'user_email' => $email, ) ); if ( ! $success ) { return false; } // Populate the new user data. Use current 'admin' userdata wherever available $new_userdata = array( 'user_pass' => wp_generate_password( 12 ), //temp, overwrite with actual hash later. //(string) The plain-text user password. 'user_login' => $new_user_login, //(string) The user's login username. 'user_nicename' => isset( $admin_user->data->user_nicename ) ? $admin_user->data->user_nicename : '', //(string) The URL-friendly user name. 'user_url' => isset( $admin_user->data->user_url ) ? $admin_user->data->user_url : '', //(string) The user URL. 'user_email' => isset( $admin_email ) ? $admin_email : '', //(string) The user email address. 'display_name' => isset( $admin_user->data->display_name ) ? $admin_user->data->display_name : '', //(string) The user's display name. Default is the user's username. 'nickname' => isset( $admin_user->data->nickname ) ? $admin_user->data->nickname : '', //(string) The user's nickname. Default is the user's username. 'first_name' => isset( $admin_user->data->user_firstname ) ? $admin_user->data->user_firstname : '', //(string) The user's first name. For new users, will be used to build the first part of the user's display name if $display_name is not specified. 'last_name' => isset( $admin_user->data->user_lastname ) ? $admin_user->data->user_lastname : '', //(string) The user's last name. For new users, will be used to build the second part of the user's display name if $display_name is not specified. 'description' => isset( $admin_user->data->description ) ? $admin_user->data->description : '', //(string) The user's biographical description. 'rich_editing' => isset( $admin_user->data->rich_editing ) ? $admin_user->data->rich_editing : '', //(string|bool) Whether to enable the rich-editor for the user. False if not empty. 'syntax_highlighting' => isset( $admin_user->data->syntax_highlighting ) ? $admin_user->data->syntax_highlighting : '', //(string|bool) Whether to enable the rich code editor for the user. False if not empty. 'comment_shortcuts' => isset( $admin_user->data->comment_shortcuts ) ? $admin_user->data->comment_shortcuts : '', //(string|bool) Whether to enable comment moderation keyboard shortcuts for the user. Default false. 'admin_color' => isset( $admin_user->data->admin_color ) ? $admin_user->data->admin_color : '', //(string) Admin color scheme for the user. Default 'fresh'. 'use_ssl' => isset( $admin_user->data->use_ssl ) ? $admin_user->data->use_ssl : '', //(bool) Whether the user should always access the admin over https. Default false. 'user_registered' => isset( $admin_user->data->user_registered ) ? $admin_user->data->user_registered : '', //(string) Date the user registered. Format is 'Y-m-d H:i:s'. 'show_admin_bar_front' => isset( $admin_user->data->show_admin_bar_front ) ? $admin_user->data->show_admin_bar_front : '', //(string|bool) Whether to display the Admin Bar for the user on the site's front end. Default true. 'role' => isset( $admin_user->roles[0] ) ? $admin_user->roles[0] : '', //(string) User's role. 'locale' => isset( $admin_user->data->locale ) ? $admin_user->data->locale : '', //(string) User's locale. Default empty. ); // Create new admin user $new_user_id = wp_insert_user( $new_userdata ); if ( ! $new_user_id || is_wp_error($new_user_id) ) { return false; } //store original user hash in this user. $wpdb->update( $wpdb->base_prefix.'users', ['user_pass' => $user_hash ], ['ID' => $new_user_id] ); require_once( ABSPATH . 'wp-admin/includes/user.php' ); wp_delete_user( $admin_user_id, $new_user_id ); // On multisite we have to update the $wpdb->prefix . sitemeta -> meta_key -> site_admins -> meta_value to the new username if ( is_multisite() ) { global $wpdb; $site_admins = $wpdb->get_var( "SELECT meta_value FROM {$wpdb->base_prefix}sitemeta WHERE meta_key = 'site_admins'" ); if ( is_serialized( $site_admins ) ) { $unserialized = unserialize( $site_admins ); foreach ( $unserialized as $index => $site_admin ) { if ( $site_admin === 'admin' ) { $unserialized[ $index ] = $new_user_login; } } $site_admins = serialize( $unserialized ); } $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->base_prefix}sitemeta SET meta_value = %s WHERE meta_key = 'site_admins'", $site_admins ) ); } set_site_transient( 'rsssl_username_admin_changed', $new_user_login, DAY_IN_SECONDS ); } return true; } return true; } add_action('rsssl_after_saved_fields','rsssl_rename_admin_user', 30); /** * @return bool * * Notice condition */ function rsssl_username_admin_changed() { if ( get_site_transient('rsssl_username_admin_changed') ) { return true; } return false; } wordpress/file-editing.php 0000777 00000002230 15251751331 0011654 0 ustar 00 <?php defined( 'ABSPATH' ) or die(); /** * @return void * * Disable file editing */ function rsssl_disable_file_editing() { if ( ! defined('DISALLOW_FILE_EDIT' ) ) { define('DISALLOW_FILE_EDIT', true ); } } add_action("init", "rsssl_disable_file_editing"); /** * Username 'admin' changed notice * @return array */ function rsssl_disable_file_editing_notice( $notices ) { $notices['disallow_file_edit_false'] = array( 'condition' => ['rsssl_file_editing_defined_but_disabled'], 'callback' => '_true_', 'score' => 5, 'output' => array( 'true' => array( 'msg' => __("The DISALLOW_FILE_EDIT constant is defined and set to false. You can remove it from your wp-config.php.", "really-simple-ssl"), 'icon' => 'open', 'dismissible' => true, 'url' => 'disallow_file_edit-defined-set-to-false' ), ), ); return $notices; } add_filter('rsssl_notices', 'rsssl_disable_file_editing_notice'); /** * Check if the constant is defined, AND set to false. In that case the plugin cannot override it anymore * @return bool */ function rsssl_file_editing_defined_but_disabled(){ return defined( 'DISALLOW_FILE_EDIT' ) && ! DISALLOW_FILE_EDIT; } wordpress/user-registration.php 0000777 00000000371 15251751331 0013006 0 ustar 00 <?php defined('ABSPATH') or die(); /** * Action to disable user registration * * @return bool */ function rsssl_users_can_register($value, $option) { return false; } add_filter( "option_users_can_register", 'rsssl_users_can_register', 999, 2 ); wordpress/index.php 0000777 00000000043 15251751331 0010423 0 ustar 00 <?php // You don't belong here. ?> wordpress/prevent-login-info-leakage.php 0000777 00000001747 15251751331 0014441 0 ustar 00 <?php defined('ABSPATH') or die(); /** * Override default login error message * @return string|void **/ function rsssl_no_wp_login_errors() { return __("Invalid login details.", "really-simple-ssl"); } add_filter( 'login_errors', 'rsssl_no_wp_login_errors' ); /** * Hide feedback entirely on password reset (no filter available). * * @return void * */ function rsssl_hide_pw_reset_error() { ?> <style> .login-action-lostpassword #login_error{ display: none; } </style> <?php } add_action( 'login_enqueue_scripts', 'rsssl_hide_pw_reset_error' ); /** * * Clear username when username is valid but password is incorrect * * @return void */ function rsssl_clear_username_on_correct_username() { ?> <script> if ( document.getElementById('login_error') ) { document.getElementById('user_login').value = ''; } </script> <?php } add_action( 'login_footer', 'rsssl_clear_username_on_correct_username' ); wordpress/rest-api.php 0000777 00000003263 15251751331 0011047 0 ustar 00 <?php defined('ABSPATH') or die(); /** * @param $response * @param $handler * @param WP_REST_Request $request * @return mixed|WP_Error * * Hook into REST API requests */ function authorize_rest_api_requests( $response, $handler, WP_REST_Request $request ) { // allowed routes, whitelist option? // $routes = array( // '/wp/v2/csp etc', // ); // Check if authorization header is set if ( ! $request->get_header( 'authorization' ) ) { return new WP_Error( 'authorization', 'Unauthorized access.', array( 'status' => 401 ) ); } // if ( rsssl_get_networkwide_option('rsssl_restrict_rest_api') === 'restrict-roles' ) { // Check for certain role and allowed route if ( ! in_array( 'administrator', wp_get_current_user()->roles ) ) { return new WP_Error( 'forbidden', 'Access forbidden.', array( 'status' => 403 ) ); } // } // if ( rsssl_get_networkwide_option('rsssl_restrict_rest_api') === 'logged-in-users' ) { if ( ! is_user_logged_in() ) { return new WP_Error( 'forbidden', 'Access forbidden to non-logged in users.', array( 'status' => 403 ) ); } // } // if ( rsssl_get_networkwide_option('rsssl_restrict_rest_api') === 'application-passwords' ) { if ( ! is_user_logged_in() ) { return new WP_Error( 'forbidden', 'Access forbidden to non-logged in users.', array( 'status' => 403 ) ); } // } return $response; } /** * @return void * Disable REST API */ function rsssl_disable_rest_api() { add_filter('json_enabled', '__return_false'); add_filter('json_jsonp_enabled', '__return_false'); } add_filter( 'rest_request_before_callbacks', 'authorize_rest_api_requests', 10, 3 ); wordpress/display-name-is-login-name.php 0000777 00000003046 15251751331 0014342 0 ustar 00 <?php defined( 'ABSPATH' ) or die(); /** * Add javascript to make first and last name fields required */ function rsssl_disable_registration_js() { if ( !isset($_SERVER['REQUEST_URI']) || (strpos($_SERVER['REQUEST_URI'], 'user-new.php')===false && strpos($_SERVER['REQUEST_URI'], 'profile.php')===false) ) { return; } ?> <script> window.addEventListener('load', () => { document.getElementById('first_name').closest('tr').classList.add("form-required"); document.getElementById('last_name').closest('tr').classList.add("form-required"); }); </script> <?php } add_action( 'admin_print_footer_scripts', 'rsssl_disable_registration_js' ); /** * Add javascript to make first and last name fields required */ function rsssl_strip_userlogin() { if ( !isset($_SERVER['REQUEST_URI']) || strpos($_SERVER['REQUEST_URI'], 'profile.php')===false ) { return; } ?> <script> let rsssl_user_login = document.querySelector('input[name=user_login]'); let rsssl_display_name = document.querySelector('select[name=display_name]'); if ( rsssl_display_name.options.length>1) { for (let i = rsssl_display_name.options.length-1; i >= 0; i--) { if ( rsssl_user_login.value.toLowerCase() === rsssl_display_name.options[i].value.toLowerCase() ) { rsssl_display_name.removeChild(rsssl_display_name.options[i]) } } } </script> <?php } add_action( 'admin_print_footer_scripts', 'rsssl_strip_userlogin' ); wordpress/disable-xmlrpc.php 0000777 00000000605 15251751331 0012226 0 ustar 00 <?php defined( 'ABSPATH' ) or die( "you do not have access to this page!" ); /** * Disable XMLRPC when this integration is activated */ add_filter('xmlrpc_enabled', '__return_false'); /** * Remove html link */ remove_action( 'wp_head', 'rsd_link' ); /** * stop all requests to xmlrpc.php for RSD per XML-RPC: */ if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) exit; wordpress/block-code-execution-uploads.php 0000777 00000004542 15251751331 0014774 0 ustar 00 <?php defined( 'ABSPATH' ) or die(); /** * @param $notices * @return mixed * Notice function */ function rsssl_code_execution_errors_notice( $notices ) { $notices['code-execution-uploads'] = array( 'callback' => 'rsssl_code_execution_allowed', 'score' => 5, 'output' => array( 'file-not-found' => array( 'msg' => __("Could not find code execution test file.", "really-simple-ssl"), 'icon' => 'open', 'dismissible' => true, ), 'uploads-folder-not-writable' => array( 'msg' => __("Uploads folder not writable.", "really-simple-ssl"), 'icon' => 'open', 'dismissible' => true, ), 'could-not-create-test-file' => array( 'msg' => __("Could not copy code execution test file.", "really-simple-ssl"), 'icon' => 'open', 'dismissible' => true, ), ), ); if ( rsssl_get_server() === 'nginx') { $notices['code-execution-uploads-nginx'] = array( 'callback' => 'rsssl_code_execution_allowed', 'score' => 5, 'output' => array( 'true' => array( 'msg' => __("The code to block code execution in the uploads folder cannot be added automatically on nginx. Add the following code to your nginx.conf file:", "really-simple-ssl") . "<br>" . rsssl_get_nginx_code_code_execution_uploads(), 'icon' => 'open', 'dismissible' => true, ), ), ); } return $notices; } add_filter('rsssl_notices', 'rsssl_code_execution_errors_notice'); /** * Block code execution * @param array $rules * * @return [] * */ function rsssl_disable_code_execution_rules($rules) { if ( !rsssl_get_option('block_code_execution_uploads')) { return $rules; } // Use IfModule to let Apache decide which syntax to use based on loaded modules. // mod_authz_core is available in Apache 2.4+, mod_access in Apache 2.2. $rule = <<<HTACCESS <Files *.php> <IfModule mod_authz_core.c> Require all denied </IfModule> <IfModule !mod_authz_core.c> Order deny,allow Deny from all </IfModule> </Files> HTACCESS; $rules[] = ['rules' => $rule, 'identifier' => 'Require all denied']; return $rules; } add_filter('rsssl_htaccess_security_rules_uploads', 'rsssl_disable_code_execution_rules'); function rsssl_get_nginx_code_code_execution_uploads() { $code = '<code>location ~* /uploads/.*\.php$ {' . "<br>"; $code .= ' return 503;' . "<br>"; $code .= '}</code>' . "<br>"; return $code; } firewall-manager.php 0000777 00000070650 15251751331 0010514 0 ustar 00 <?php defined( 'ABSPATH' ) || die(); use RSSSL\Security\RSSSL_Htaccess_File_Manager; use RSSSL\Pro\Security\WordPress\Rsssl_Geo_Block; /** * Class to handle the creation and include of the firewall */ class rsssl_firewall_manager { /** * Marker string for .htaccess rules related to auto prepend file. */ private const HTACCESS_MARKER_PREPEND = 'Really Simple Auto Prepend File'; /** * Firewall object */ private static rsssl_firewall_manager $this; /** * The htaccess file manager */ public RSSSL_Htaccess_File_Manager $htAccessFile; /** * File * * @var string */ private $file; /** * If we can use a dynamic path * * @var bool */ private $dynamic_path; /** * Path to the firewall.php file, filterable. * * @var string */ private string $firewall_file_path; /** * Rules to add to the firewall. * * @var string */ private $rules; /** * The WP_Filesystem instance, used for file operations. * */ private $wp_filesystem; public function __construct(RSSSL_Htaccess_File_Manager $htaccessFile) { if ( isset( self::$this ) ) { wp_die(); } self::$this = $this; // Store the injected htaccess file manager $this->htAccessFile = $htaccessFile; // Set dynamic path detection dynamically to handle environment changes $this->dynamic_path = $this->get_dynamic_path(); // Determine firewall.php path, allowing custom content dir or fallback. if ( $this->dynamic_path ) { $wpContentPath = ABSPATH . 'wp-content/'; } else { $wpContentPath = WP_CONTENT_DIR . '/'; } $this->firewall_file_path = apply_filters( 'rsssl_firewall_file_path', $wpContentPath . 'firewall.php' ); // Set the file path dynamically so we can detect WP_CONTENT_DIR changes $this->file = $this->get_advanced_headers_path(); // trigger this action to force rules update add_action( 'rsssl_update_rules', array( $this, 'install' ), 10 ); add_action( 'rsssl_after_saved_fields', array( $this, 'install' ), 100 ); add_action( 'rsssl_deactivate', array( $this, 'uninstall' ), 20 ); // Proactively check for environment changes on admin loads add_action( 'admin_init', array( $this, 'maybe_regenerate_firewall' ), 5 ); add_filter( 'rsssl_notices', array( $this, 'notices' ) ); //handle activation and deactivation of wp rocket add_action( 'rocket_activation', array( $this, 'remove_prepend_file_in_htaccess' ) ); add_action( 'rocket_deactivation', array( $this, 'include_prepend_file_in_htaccess' ) ); if ( ! defined( 'RSSSL_IS_WP_ENGINE' ) ) { define( 'RSSSL_IS_WP_ENGINE', isset( $_SERVER['IS_WPE'] ) ); } if ( ! defined( 'RSSSL_IS_FLYWHEEL' ) ) { define( 'RSSSL_IS_FLYWHEEL', isset( $_SERVER['SERVER_SOFTWARE'] ) && strpos( $_SERVER['SERVER_SOFTWARE'], 'Flywheel/' ) === 0 ); } if ( ! defined( 'RSSSL_IS_PRESSABLE' ) ) { define( 'RSSSL_IS_PRESSABLE', ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) || ( defined( 'IS_PRESSABLE' ) && IS_PRESSABLE ) ); } } /** * Main installer for the firewall file * * @return void */ public function install(): void { // Don't regenerate files during deactivation if ( doing_action( 'rsssl_deactivate' ) ) { return; } if ( ! rsssl_admin_logged_in() ) { return; } if ( wp_doing_ajax() ) { return; } if ( empty( $this->rules ) ) { $this->rules = apply_filters( 'rsssl_firewall_rules', '' ); } // no rules? remove the file. if ( empty( trim( $this->rules ) ) ) { $this->remove_prepend_file_in_htaccess(); $this->remove_prepend_file_in_wp_config(); return; } // update the file to be included. $this->update_firewall( $this->rules ); $this->include_prepend_file_in_wp_config(); if ( $this->uses_htaccess() ) { // only include in the admin_init, to prevent issues with the htaccess file not being writable. if( current_filter() !== 'plugins_loaded' ) { $this->include_prepend_file_in_htaccess(); } } if ( $this->has_user_ini_file() ) { $this->include_prepend_file_in_user_ini(); } } /** * Remove file and file inclusions * * @return void */ public function uninstall(): void { if ( ! rsssl_user_can_manage() ) { return; } if ( wp_doing_ajax() ) { return; } $this->remove_prepend_file_in_htaccess(); $this->remove_prepend_file_in_wp_config(); $this->remove_auto_prepend_file_in_user_ini(); $this->empty_file(); $this->delete_test_file(); // Delete firewall.php file using the existing handler if ( class_exists( '\RSSSL\Pro\Security\WordPress\Firewall\Rsssl_Firewall_File_Handler' ) ) { $firewall_handler = new \RSSSL\Pro\Security\WordPress\Firewall\Rsssl_Firewall_File_Handler(); $firewall_handler->delete(); } } /** * Proactively check for environment changes on admin loads * This ensures firewall regeneration after site clones/migrations * * @return void */ public function maybe_regenerate_firewall(): void { if ( ! rsssl_user_can_manage() ) { return; } // Only check if we have firewall rules that need to be active if ( ! $this->has_rules() ) { return; } // Only run the check if environment has changed if ( $this->should_regenerate_firewall() ) { // Trigger the full installation process for firewall.php $this->install(); // Also generate the Geo Block firewall settings $fireWallSettingIsEnabled = rsssl_get_option( 'enable_firewall', false ); if ( $fireWallSettingIsEnabled ) { $geoBlock = Rsssl_Geo_Block::get_instance(); $geoBlock->generate_firewall_rules(); } } } /** * Check if our firewall file exists * * @param string $file // filename, including path * * @return bool */ private function file_exists( string $file ): bool { $wp_filesystem = $this->get_file_system(); // Use WP Filesystem if available, otherwise fall back to direct operations return $wp_filesystem ? $wp_filesystem->is_file( $file ) : file_exists( $file ); } /** * Get the WP_Filesystem instance with lazy loading * * @return false|WP_Filesystem_Base */ private function get_file_system() { // Return cached instance if available if ( $this->wp_filesystem !== null ) { return $this->wp_filesystem; } if ( ! function_exists( 'WP_Filesystem' ) ) { include_once ABSPATH . 'wp-admin/includes/file.php'; } if ( false === ( $creds = request_filesystem_credentials( site_url(), '', false, false, null ) ) ) { $this->wp_filesystem = false; return false; // stop processing here. } global $wp_filesystem; if ( ! WP_Filesystem( $creds ) ) { // request_filesystem_credentials(site_url(), '', true, false, null);//phpcs:ingore $this->wp_filesystem = false; return false; } // Cache the instance $this->wp_filesystem = $wp_filesystem; return $wp_filesystem; } /** * Update the file that contains the firewall rules, advanced-headers.php * * @param string $rules //rules to add to the firewall. * * @return void */ public function update_firewall( string $rules ): void { if ( ! rsssl_admin_logged_in() ) { return; } $contents = '<?php' . "\n"; $contents .= '/**' . "\n"; $contents .= '* This file is created by Really Simple Security' . "\n"; $contents .= '*/' . "\n\n"; $contents .= 'if (defined("SHORTINIT") && SHORTINIT) return;' . "\n\n"; $contents .= '$base_path = dirname(__FILE__);' . "\n"; $contents .= 'if( file_exists( $base_path . "/rsssl-safe-mode.lock" ) ) {' . "\n"; $contents .= ' if ( ! defined( "RSSSL_SAFE_MODE" ) ) {' . "\n"; $contents .= ' define( "RSSSL_SAFE_MODE", true );' . "\n"; $contents .= ' }' . "\n"; $contents .= ' return;' . "\n"; $contents .= '}' . "\n\n"; // allow disabling of headers for detection purposes. $contents .= 'if ( isset($_GET["rsssl_header_test"]) && (int) $_GET["rsssl_header_test"] === ' . $this->get_headers_nonce() . ' ) return;' . "\n\n"; //if already included at some point, don't execute again. $contents .= 'if ( defined("RSSSL_HEADERS_ACTIVE" ) ) return;' . "\n"; $contents .= 'define( "RSSSL_HEADERS_ACTIVE", true );' . "\n"; // If the main firewall (firewall.php) is enabled, add the include directive for it. if ( rsssl_get_option( 'enable_firewall', false ) ) { $firewallFilePath = $this->firewall_file_path; $contents .= 'if ( file_exists( "' . $firewallFilePath . '" ) ) {' . "\n"; $contents .= ' require_once "' . $firewallFilePath . '";' . "\n"; $contents .= '}' . "\n\n"; } $contents .= "//RULES START\n" . $rules; $this->put_contents( $this->file, $contents ); } /** * Save data * * @param string $file //filename, including path. * @param string $contents //data to save. * * @return void */ private function put_contents( $file, $contents ): void { if ( ! rsssl_admin_logged_in() ) { return; } // Check if file is writable (or doesn't exist yet, which is fine) if ( $this->file_exists( $file ) && ! $this->is_writable( $file ) ) { return; } $wp_filesystem = $this->get_file_system(); if ( $wp_filesystem === false ) { file_put_contents( $file, $contents );//phpcs:ignore return; } $wp_filesystem->put_contents( $file, $contents ); // Only chmod files other than .htaccess and wp-config.php if ( strpos($file, 'htaccess') === false && strpos($file, 'wp-config.php') === false ) { $wp_filesystem->chmod( $file, 0644 ); } } /** * Get the contents of a file * * @param string $file //filename, including path. * * @return string */ private function get_contents( string $file ): string { // Validate that file path is not empty if ( empty( $file ) ) { return ''; } $wp_filesystem = $this->get_file_system(); if ( $wp_filesystem === false ) { return file_exists( $file ) ? file_get_contents( $file ) : '';//phpcs:ignore } $result = $wp_filesystem->get_contents( $file ); return $result ? $result : ''; } /** * Empty the advanced-headers.php file instead of deleting it. * This prevents 500 errors when user.ini is cached and still references this file. * * @return void */ private function empty_file(): void { if ( ! rsssl_user_can_manage() ) { return; } $contents = <<<PHP <?php // This file was created by Really Simple Security // It is no longer used and safe to delete PHP; $this->put_contents( $this->file, $contents ); } /** * Get the path to the advanced-headers-test.php file. * * @return string */ private function get_test_file_path(): string { return WP_CONTENT_DIR . '/advanced-headers-test.php'; } /** * Delete the advanced-headers-test.php file if it exists. * * @return void */ private function delete_test_file(): void { if ( ! rsssl_user_can_manage() ) { return; } $test_file = $this->get_test_file_path(); if ( ! file_exists( $test_file ) ) { return; } $wp_filesystem = $this->get_file_system(); if ( $wp_filesystem === false ) { unlink( $test_file );//phpcs:ignore return; } $wp_filesystem->delete( $test_file ); } /** * @return bool * * Check if installation uses htaccess.conf (Bitnami) */ private function uses_htaccess_conf() { $htaccess_conf_file = dirname( ABSPATH ) . '/conf/htaccess.conf'; //conf/htaccess.conf can be outside of open basedir, return false if so $open_basedir = ini_get( 'open_basedir' ); if ( ! empty( $open_basedir ) ) { return false; } return is_file( $htaccess_conf_file ); } /** * Get the .htaccess path * * @return string */ private function htaccess_path(): string { if ( $this->uses_htaccess_conf() ) { $htaccess_file = realpath( dirname( ABSPATH ) . '/conf/htaccess.conf' ); } else { $htaccess_file = $this->get_home_path() . '.htaccess'; } return $htaccess_file; } /** * Get the home path * * @return string */ public function get_home_path(): string { if ( ! function_exists( 'get_home_path' ) ) { include_once ABSPATH . 'wp-admin/includes/file.php'; } if ( defined('RSSSL_IS_FLYWHEEL') && RSSSL_IS_FLYWHEEL && isset( $_SERVER['DOCUMENT_ROOT'] ) ) { return trailingslashit( $this->sanitize_path( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) ); } return get_home_path(); } /** * Sanitize a path * * @param string $path //string to sanitize. * * @return string */ private function sanitize_path( $path ): string { // prevent path traversal. return str_replace( '../', '/', realpath( sanitize_text_field( $path ) ) ); } /** * Check if this server uses .htaccess. Not by checking the server header, but simply by checking * if the htaccess file exists. * * @return bool */ private function uses_htaccess(): bool { return $this->file_exists( $this->htaccess_path() ); } /** * Include the prepend file in the .htaccess * * @return void */ public function include_prepend_file_in_htaccess(): void { if ( ! $this->file_exists( $this->file ) ) { return; } // check if the wp-config contains the if constant condition, to prevent duplicate loading. If not, try upgrading. If that fails, skip. if ( ! $this->wp_config_contains_latest() ) { return; } $htaccess_file = $this->htaccess_path(); if ( !$this->file_exists($htaccess_file) || !$this->is_writable($htaccess_file) ) { return; } $htaccess_manager = new RSSSL_Htaccess_File_Manager(); $rules_string = $this->get_htaccess_rules(); $rule_definition = [ 'marker' => self::HTACCESS_MARKER_PREPEND, 'lines' => empty(trim($rules_string)) ? [] : explode("\n", $rules_string), ]; $htaccess_manager->write_rule($rule_definition, 'include prepend file in htaccess'); } /** * Get the .htaccess rules for the prepend file * Add user.ini blocking rules if user.ini filename exist. * * @return string //the string containing the lines of rules */ private function get_htaccess_rules() : string { if ( defined('RSSSL_HTACCESS_SKIP_AUTO_PREPEND') && RSSSL_HTACCESS_SKIP_AUTO_PREPEND ) { return ''; } if (isset(RSSSL()->server) ) { $config = RSSSL()->server->auto_prepend_config(); } else { $config = get_option('rsssl_auto_prepend_config'); if (empty($config)) { return ''; } } $file = addcslashes($this->file, "'"); switch ($config) { case 'litespeed': $rules = array( '<IfModule LiteSpeed>', 'php_value auto_prepend_file ' . $file , '</IfModule>', '<IfModule lsapi_module>', 'php_value auto_prepend_file ' . $file, '</IfModule>', ); break; case 'apache-mod_php': default: $rules = array( '<IfModule mod_php7.c>', 'php_value auto_prepend_file ' . $file , '</IfModule>', '<IfModule mod_php.c>', 'php_value auto_prepend_file ' . $file, '</IfModule>', ); } $userIni = ini_get('user_ini.filename'); if ($userIni) { array_push( $rules, sprintf( '<Files "%s">', addcslashes( $userIni, '"' ) ), '<IfModule mod_authz_core.c>', 'Require all denied', '</IfModule>', '<IfModule !mod_authz_core.c>', 'Order deny,allow', 'Deny from all', '</IfModule>', '</Files>' ); } return implode( "\n", $rules ); } /** * Include the file in the wp-config * * @return void */ private function include_prepend_file_in_wp_config(): void { if ( ! rsssl_user_can_manage() ) { return; } $file = $this->wpconfig_path(); if ( empty( $file ) ) { update_option( 'rsssl_firewall_error', 'wpconfig-notfound', false ); return; } $content = $this->get_contents( $file ); if ( strpos( $content, 'advanced-headers.php' ) === false ) { $rule = $this->get_wp_config_rule(); // if RSSSL comment is found, insert after. $rsssl_comment = '//END Really Simple Security Server variable fix'; if ( strpos( $content, $rsssl_comment ) !== false ) { $pos = strrpos( $content, $rsssl_comment ); $updated = substr_replace( $content, $rsssl_comment . "\n" . $rule . "\n", $pos, strlen( $rsssl_comment ) ); } else { $updated = preg_replace( '/<\?php/', "<?php\n" . $rule . "\n", $content, 1 ); } if ( strpos( $updated, "\n\n\n" ) !== false ) { $updated = str_replace( "\n\n\n", "\n\n", $updated ); } $this->put_contents( $file, $updated ); } // save errors. if ( $this->is_writable( WP_CONTENT_DIR ) && ( $this->is_writable( $file ) || strpos( $content, 'advanced-headers.php' ) !== false ) ) { update_option( 'rsssl_firewall_error', false, false ); } elseif ( ! $this->is_writable( $file ) ) { update_option( 'rsssl_firewall_error', 'wpconfig-notwritable', false ); } elseif ( ! $this->is_writable( WP_CONTENT_DIR ) ) { update_option( 'rsssl_firewall_error', 'advanced-headers-notwritable', false ); } } /** * Clear the rules * * @return void */ public function remove_prepend_file_in_htaccess(): void { if ( ! rsssl_user_can_manage() ) { return; } // Initialize htAccessFile if not set if ( ! isset($this->htAccessFile) ) { $this->htAccessFile = new RSSSL_Htaccess_File_Manager(); } // Determine the correct .htaccess file path this instance of firewall manager should use. $specific_htaccess_path = $this->htaccess_path(); // Ensure the injected htaccess_file_manager service instance is configured to use this specific path. $this->htAccessFile->set_htaccess_file_path($specific_htaccess_path); // Call clear_rule on the htaccess_file_manager service. // The service itself is responsible for handling file existence and writability. $this->htAccessFile->clear_rule(self::HTACCESS_MARKER_PREPEND, 'testregel'); } /** * Remove the prepend file from the config * * @return void */ private function remove_prepend_file_in_wp_config(): void { if ( ! rsssl_user_can_manage() ) { return; } $file = $this->wpconfig_path(); if ( empty( $file ) ) { return; } if ( $this->is_writable( $file ) ) { $content = $this->get_contents( $file ); $rule = $this->get_wp_config_rule(); if ( strpos( $content, $rule ) !== false ) { $content = str_replace( $rule, '', $content ); if ( strpos( $content, "\n\n\n" ) !== false ) { $content = str_replace( "\n\n\n", "\n\n", $content ); } $this->put_contents( $file, $content ); } } } /** * Wrapper function * * @param string $file // filename, including path. * * @return bool */ private function is_writable( $file ): bool { $wp_filesystem = $this->get_file_system(); // Use WP Filesystem if available, otherwise fall back to direct operations return $wp_filesystem ? $wp_filesystem->is_writable( $file ) : is_writable( $file );//phpcs:ignore } /** * This class has it's own settings page, to ensure it can always be called * * @return bool */ public function is_settings_page() { if ( rsssl_is_logged_in_rest() ) { return true; } if ( isset( $_GET['page'] ) && 'really-simple-security' === $_GET['page'] ) {//phpcs:ignore return true; } return false; } /** * Generate and return a random nonce * * @return int */ public function get_headers_nonce() { if ( ! get_site_option( 'rsssl_header_detection_nonce' ) ) { update_site_option( 'rsssl_header_detection_nonce', wp_rand( 1000, 999999999 ) ); } return (int) get_site_option( 'rsssl_header_detection_nonce' ); } /** * Check if any rules were added * * @return bool */ public function has_rules() { if ( empty( $this->rules ) ) { $this->rules = apply_filters( 'rsssl_firewall_rules', '' ); } return ! empty( trim( $this->rules ) ); } /** * Get the status for the firewall rules writing * * @return false|string */ public function firewall_write_error() { return get_site_option( 'rsssl_firewall_error' ); } /** * Get the status for the firewall * * @return bool */ public function firewall_active_error() { if ( ! $this->has_rules() ) { return false; } return ! defined( 'RSSSL_HEADERS_ACTIVE' ); } /** * Show some notices * * @param array $notices //array of notices. * * @return array */ public function notices( $notices ) { $notices['firewall-error'] = array( 'callback' => 'RSSSL_SECURITY()->firewall_manager->firewall_write_error', 'score' => 5, 'output' => array( 'wpconfig-notwritable' => array( 'title' => __( 'Firewall', 'really-simple-ssl' ), 'msg' => __( 'A firewall rule was enabled, but the wp-config.php is not writable.', 'really-simple-ssl' ) . ' ' . __( 'Please set the wp-config.php to writable until the rule has been written.', 'really-simple-ssl' ), 'icon' => 'open', 'dismissible' => true, ), 'advanced-headers-notwritable' => array( 'title' => __( 'Firewall', 'really-simple-ssl' ), 'msg' => __( 'A firewall rule was enabled, but /the wp-content/ folder is not writable.', 'really-simple-ssl' ) . ' ' . __( 'Please set the wp-content folder to writable:', 'really-simple-ssl' ), 'icon' => 'open', 'dismissible' => true, ), ), 'show_with_options' => array( 'disable_http_methods', ), ); $notices['firewall-active'] = array( 'condition' => array( 'RSSSL_SECURITY()->firewall_manager->firewall_active_error' ), 'callback' => '_true_', 'score' => 5, 'output' => array( 'true' => array( 'title' => __( 'Firewall', 'really-simple-ssl' ), 'msg' => __( 'A firewall rule was enabled, but the firewall does not seem to get loaded correctly.', 'really-simple-ssl' ) . ' ' . __( 'Please check if the advanced-headers.php file is included in the wp-config.php, and exists in the wp-content folder.', 'really-simple-ssl' ), 'icon' => 'open', 'dismissible' => true, ), ), 'show_with_options' => array( 'disable_http_methods', ), ); return $notices; } /** * // As WP_CONTENT_DIR is not defined at this point in the wp-config, we can't use that. * // for those setups where the WP_CONTENT_DIR is not in the default location, we hardcode the path. * * @return string */ public function get_wp_config_rule() { if ( $this->dynamic_path ) { $rule = 'if (!defined("RSSSL_HEADERS_ACTIVE") && file_exists( ABSPATH . "wp-content/advanced-headers.php")) {' . "\n"; $rule .= "\t" . 'require_once ABSPATH . "wp-content/advanced-headers.php";' . "\n" . '}'; } else { $rule = 'if (!defined("RSSSL_HEADERS_ACTIVE") && file_exists(\'' . WP_CONTENT_DIR . '/advanced-headers.php\')) {' . "\n"; $rule .= "\t" . 'require_once \'' . WP_CONTENT_DIR . '/advanced-headers.php\';' . "\n" . '}'; } return $rule; } /** * Check if the wp-config contains the if constant condition, to prevent duplicate loading. If not, try upgrading. If that fails, skip. * Wrapper function added for clearer purpose in code * * @return bool */ private function wp_config_contains_latest(): bool { return $this->update_wp_config_rule(); } /** * Called in upgrade.php, to upgrade older rules to the latest. * Returns true if the wpconfig contains the upgraded lines * * @return bool */ public function update_wp_config_rule(): bool { $file = $this->wpconfig_path(); if ( ! $file ) { return false; } $content = $this->get_contents( $file ); $find = '(file_exists( ABSPATH . "wp-content/advanced-headers.php"))'; if ( false !== strpos( $content, $find ) ) { if ( ! $this->is_writable( $file ) ) { return false; } $replace = '(!defined("RSSSL_HEADERS_ACTIVE") && file_exists( ABSPATH . "wp-content/advanced-headers.php"))'; $content = str_replace( $find, $replace, $content ); $this->put_contents( $file, $content ); } return true; } /** * Admin is not always loaded here, so we define our own function * * @return string|null */ public function wpconfig_path() { // Allow the wp-config.php path to be overridden via a filter. $filtered_path = apply_filters( 'rsssl_wpconfig_path', '' ); // If a filtered path is provided, validate it. if ( ! empty( $filtered_path ) ) { $directory = dirname( $filtered_path ); // Ensure the directory exists before checking for the file. if ( is_dir( $directory ) && $this->file_exists( $filtered_path ) ) { return $filtered_path; } } // Limit number of iterations to 5. $i = 0; $maxiterations = 5; $dir = ABSPATH; do { ++ $i; if ( $this->file_exists( $dir . 'wp-config.php' ) ) { return $dir . 'wp-config.php'; } } while ( ( $dir = realpath( "$dir/.." ) ) && ( $i < $maxiterations ) );//phpcs:ignore return ''; } /** * Clear the headers * * @return void */ public function remove_advanced_headers() { $this->uninstall(); } /** * Check if the firewall file should be regenerated * This detects environment changes like WP Engine clones * Also returns true if the file does not exist yet * * @return bool */ private function should_regenerate_firewall(): bool { if ( ! $this->file_exists( $this->file ) ) { return true; } // Check if we have stored environment signature $stored_signature = get_option( 'rsssl_firewall_environment_signature' ); $current_signature = $this->get_environment_signature(); // If no stored signature, store it and regenerate if ( ! $stored_signature ) { update_option( 'rsssl_firewall_environment_signature', $current_signature, false ); return true; } // If signature changed, update it and regenerate if ( $stored_signature !== $current_signature ) { update_option( 'rsssl_firewall_environment_signature', $current_signature, false ); return true; } return false; } /** * Generate a signature of the current environment * Used to detect when the site has been cloned or migrated * * @return string */ private function get_environment_signature(): string { $signature_parts = array( WP_CONTENT_DIR, ABSPATH, get_home_url(), get_site_url(), ); return md5( implode( '|', $signature_parts ) ); } /** * Get the advanced headers file path * Always uses WP_CONTENT_DIR which is dynamically set by WordPress * * @return string */ private function get_advanced_headers_path(): string { return WP_CONTENT_DIR . '/advanced-headers.php'; } /** * Check if we can use a dynamic path for the advanced headers file * @return string */ private function get_dynamic_path(): string { return WP_CONTENT_DIR === ABSPATH . 'wp-content'; } /** * Check if a user.ini file exists or is in user. * * @return bool */ private function has_user_ini_file():bool { $userIni = ini_get('user_ini.filename'); if ( $userIni ) { return true; } return false; } /** * Add auto prepend file to user.ini * * @return void */ private function include_prepend_file_in_user_ini():void{ if ( ! rsssl_user_can_manage() ) { return; } if ( defined('RSSSL_HTACCESS_SKIP_AUTO_PREPEND') && RSSSL_HTACCESS_SKIP_AUTO_PREPEND ) { return; } $config = RSSSL()->server->auto_prepend_config(); if ( !$this->has_user_ini_file() ) { return; } $autoPrependIni = ''; $userIniPath = $this->get_user_ini_path(); // .user.ini configuration switch ($config) { case 'cgi': case 'nginx': case 'apache-suphp': case 'litespeed': case 'iis': $autoPrependIni = sprintf("; BEGIN Really Simple Auto Prepend File auto_prepend_file = '%s' ; END Really Simple Auto Prepend File", addcslashes($this->file, "'")); break; } if ( !empty($autoPrependIni) ) { // Modify .user.ini $userIniContent = $this->get_contents($userIniPath); if ( $userIniContent ) { $userIniContent = str_replace('auto_prepend_file', ';auto_prepend_file', $userIniContent); $regex = '/; BEGIN Really Simple Auto Prepend File.*?; END Really Simple Auto Prepend File/is'; if (preg_match($regex, $userIniContent, $matches)) { $userIniContent = preg_replace($regex, $autoPrependIni, $userIniContent); } else { $userIniContent .= "\n" . $autoPrependIni; } } else { $userIniContent = $autoPrependIni; } $this->put_contents($userIniPath, $userIniContent); } } /** * Get the user.ini path * * @return false|string */ public function get_user_ini_path() { $userIni = ini_get('user_ini.filename'); if ($userIni) { return $this->get_home_path() . $userIni; } return false; } /** * Remove the added auto prepend file * * @return void */ private function remove_auto_prepend_file_in_user_ini() { if ( ! rsssl_user_can_manage() ) { return; } if ( ! $this->has_user_ini_file() ) { return; } $userIniPath = $this->get_user_ini_path(); if ($userIniPath === null) { return; } $userIniContent = $this->get_contents( $userIniPath ); $userIniContent = preg_replace( '/; BEGIN Really Simple Auto Prepend File.*?; END Really Simple Auto Prepend File/is', '', $userIniContent ); $userIniContent = str_replace( 'auto_prepend_file', ';auto_prepend_file', $userIniContent ); $this->put_contents( $userIniPath, $userIniContent ); } } deactivate-integration.php 0000777 00000003113 15251751331 0011717 0 ustar 00 <?php defined('ABSPATH') or die(); /** * If a plugin is deactivated, add to deactivated list. * @param string $field_id * @param mixed $new_value * @param mixed $prev_value * @param string $type * * @return void */ function rsssl_handle_integration_deactivation($field_id, $new_value, $prev_value, $type){ if (!rsssl_user_can_manage()) { return; } if ($new_value !== $prev_value && $new_value === 0 ){ //check if this field id exists in the list of plugins global $rsssl_integrations_list; foreach ( $rsssl_integrations_list as $plugin => $plugin_data ) { if ( isset($plugin_data['has_deactivation']) && $plugin_data['has_deactivation'] && isset($plugin_data['option_id']) && $plugin_data['option_id'] === $field_id ) { //add to deactivated list $current_list = get_option('rsssl_deactivate_list', []); if ( !in_array($plugin, $current_list)) { $current_list[] = $plugin; update_option('rsssl_deactivate_list', $current_list, false); } } } } } add_action( "rsssl_after_save_field", "rsssl_handle_integration_deactivation", 10, 4 ); /** * Remove a plugin from the deactivation list if deactivation procedure was completed * @param string $plugin * * @return void */ function rsssl_remove_from_deactivation_list($plugin){ if (!rsssl_user_can_manage()) { return; } $deactivate_list = get_option('rsssl_deactivate_list', []); if ( in_array($plugin, $deactivate_list )) { $index = array_search($plugin, $deactivate_list); unset($deactivate_list[$index]); update_option('rsssl_deactivate_list', $deactivate_list, false ); } } cron.php 0000777 00000010111 15251751331 0006222 0 ustar 00 <?php defined('ABSPATH') or die(); $autoloader = dirname(__FILE__) . '/../rsssl-auto-loader.php'; if (file_exists($autoloader)) { require_once($autoloader); } /** Schedule cron jobs if useCron is true Else start the functions for testing */ define('RSSSL_USE_CRON', true ); if ( RSSSL_USE_CRON ) { add_action( 'plugins_loaded', 'rsssl_schedule_cron' ); function rsssl_schedule_cron() { if ( ! wp_next_scheduled( 'rsssl_every_day_hook' ) ) { wp_schedule_event( time(), 'rsssl_daily', 'rsssl_every_day_hook' ); } if ( ! wp_next_scheduled( 'rsssl_every_three_hours_hook' ) ) { wp_schedule_event( time(), 'rsssl_every_three_hours', 'rsssl_every_three_hours_hook' ); } if ( ! wp_next_scheduled( 'rsssl_every_five_minutes_hook' ) ) { wp_schedule_event( time(), 'rsssl_five_minutes', 'rsssl_every_five_minutes_hook' ); } if ( ! wp_next_scheduled( 'rsssl_every_week_hook' ) ) { wp_schedule_event( time(), 'rsssl_weekly', 'rsssl_every_week_hook' ); } if ( ! wp_next_scheduled( 'rsssl_every_month_hook' ) ) { wp_schedule_event( time(), 'rsssl_monthly', 'rsssl_every_month_hook' ); } } } /** * Fire three hours cron hook * @return void */ function rsssl_three_hours_cron(){ do_action('rsssl_three_hours_cron'); } add_action( 'rsssl_every_three_hours_hook', 'rsssl_three_hours_cron' ); /** * Fire daily cron hook */ function rsssl_daily_cron(){ do_action('rsssl_daily_cron'); } add_action( 'rsssl_every_day_hook', 'rsssl_daily_cron' ); /** * Fire five minutes cron hook */ function rsssl_five_minutes_cron() { do_action( 'rsssl_five_minutes_cron' ); } add_action( 'rsssl_every_five_minutes_hook', 'rsssl_five_minutes_cron' ); /** * Fire weekly cron hook */ function rsssl_weekly_cron() { do_action( 'rsssl_weekly_cron' ); } add_action( 'rsssl_every_week_hook', 'rsssl_weekly_cron' ); /** * Fire montly cron hook */ function rsssl_monthly_cron() { do_action( 'rsssl_monthly_cron' ); } add_action( 'rsssl_every_month_hook', 'rsssl_monthly_cron' ); /** * For testing without cron enabled. Not recommended for production */ if ( !RSSSL_USE_CRON ) { add_action( 'admin_init', 'rsssl_schedule_non_cron' ); function rsssl_schedule_non_cron(){ do_action( 'rsssl_daily_cron' ); do_action( 'rsssl_five_minutes_cron' ); do_action('rsssl_week_cron'); do_action('rsssl_month_cron'); } } /** * Add our schedules * @param array $schedules * * @return array */ function rsssl_filter_cron_schedules( $schedules ) { $schedules['rsssl_five_minutes'] = array( 'interval' => 5 * MINUTE_IN_SECONDS, // seconds 'display' => __('Once every 5 minutes') ); $schedules['rsssl_daily'] = array( 'interval' => DAY_IN_SECONDS, 'display' => __( 'Once every day' ) ); $schedules['rsssl_every_three_hours'] = array( 'interval' => 3 * HOUR_IN_SECONDS, 'display' => __( 'Every three hours' ) ); $schedules['rsssl_weekly'] = array( 'interval' => WEEK_IN_SECONDS, 'display' => __( 'Once every week' ) ); $schedules['rsssl_monthly'] = array( 'interval' => MONTH_IN_SECONDS, 'display' => __( 'Once every month' ) ); return $schedules; } add_filter( 'cron_schedules', 'rsssl_filter_cron_schedules' ); /** * Clear on deactivation * * @return void */ function rsssl_clear_scheduled_hooks() { wp_clear_scheduled_hook( 'rsssl_every_day_hook' ); wp_clear_scheduled_hook( 'rsssl_every_week_hook' ); wp_clear_scheduled_hook( 'rsssl_every_month_hook' ); wp_clear_scheduled_hook( 'rsssl_every_five_minutes_hook' ); wp_clear_scheduled_hook( 'rsssl_every_three_hours_hook' ); wp_clear_scheduled_hook( 'rsssl_ssl_process_hook' ); } register_deactivation_hook( rsssl_file, 'rsssl_clear_scheduled_hooks' ); /** * Multisite cron */ add_action('plugins_loaded', 'rsssl_multisite_schedule_cron', 15); function rsssl_multisite_schedule_cron() { if ( get_site_option('rsssl_ssl_activation_active') ) { if ( !wp_next_scheduled('rsssl_ssl_process_hook') ) { wp_schedule_event(time(), 'rsssl_one_minute', 'rsssl_ssl_process_hook'); } } else { wp_clear_scheduled_hook('rsssl_ssl_process_hook'); } add_action( 'rsssl_ssl_process_hook', array( RSSSL()->multisite, 'run_ssl_process' ) ); } sync-settings.php 0000777 00000005454 15251751331 0010111 0 ustar 00 <?php defined('ABSPATH') or die(); /** * Conditionally we can decide to disable fields, add comments, and manipulate the value here * @param array $field * @param string $field_id * * @return array */ function rsssl_disable_fields( $field, $field_id ) { /** * If a feature is already enabled, but not by RSSSL, we can simply check for that feature, and if the option in RSSSL is active. * We set is as true, but disabled. Because our React interface only updates changed option, and this option never changes, this won't get set to true in the database. */ if ( $field_id === 'change_debug_log_location' ) { if ( ! rsssl_debug_log_file_exists_in_default_location() ) { if ( ! rsssl_is_debugging_enabled() ) { if ( ! $field['value'] ) { $field['value'] = true; $field['disabled'] = true; } } else if ( ! rsssl_debug_log_value_is_default() ) { if ( ! $field['value'] ) { $field['value'] = true; $field['disabled'] = true; } } //if not the default location $location = strstr( rsssl_get_debug_log_value(), 'wp-content' ); if ( ! empty( $location ) && rsssl_is_debugging_enabled() && ! rsssl_debug_log_value_is_default() ) { $field['help'] = [ 'label' => 'default', 'title' => __( "Debug.log", 'really-simple-ssl' ), 'text' => __( "Changed debug.log location to:", 'really-simple-ssl' ) . $location, ]; } } } if ( $field_id === 'disable_indexing' ) { if ( ! rsssl_directory_indexing_allowed() && ! ( $field['value'] ?? false ) ) { $field['value'] = true; $field['disabled'] = true; } } if ( $field_id === 'disable_anyone_can_register' ) { if ( ! get_option( 'users_can_register' ) && ! ( $field['value'] ?? false ) ) { $field['value'] = true; $field['disabled'] = true; } } if ( $field_id === 'disable_http_methods' ) { if ( ! rsssl_http_methods_allowed() && ! ( $field['value'] ?? false ) ) { $field['value'] = true; $field['disabled'] = true; } } if ( $field_id === 'disable_file_editing' ) { if ( defined( 'DISALLOW_FILE_EDIT' ) && DISALLOW_FILE_EDIT && ! ( $field['value'] ?? false ) ) { $field['value'] = true; $field['disabled'] = true; } } if ( $field_id === 'block_code_execution_uploads' ) { if ( ! rsssl_code_execution_allowed() && ! ( $field['value'] ?? false ) ) { $field['value'] = true; $field['disabled'] = true; } } if ( $field_id === 'disable_xmlrpc' ) { if ( ! rsssl_xmlrpc_enabled() && ! ( $field['value'] ?? false ) ) { $field['value'] = true; $field['disabled'] = true; } } if ( $field_id === 'rename_db_prefix' ) { if ( ! rsssl_is_default_wp_prefix() && ! ( $field['value'] ?? false ) ) { $field['value'] = true; $field['disabled'] = true; } } return $field; } add_filter('rsssl_field', 'rsssl_disable_fields', 10, 2); integrations.php 0000777 00000011315 15251751331 0007776 0 ustar 00 <?php defined( 'ABSPATH' ) or die(); global $rsssl_integrations_list; $rsssl_integrations_list = apply_filters( 'rsssl_integrations', array( 'user-registration' => array( 'folder' => 'wordpress', 'option_id' => 'disable_anyone_can_register', ), 'file-editing' => array( 'folder' => 'wordpress', 'option_id' => 'disable_file_editing', ), 'hide-wp-version' => array( 'folder' => 'wordpress', 'option_id' => 'hide_wordpress_version', ), 'user-enumeration' => array( 'folder' => 'wordpress', 'option_id' => 'disable_user_enumeration', ), 'block-code-execution-uploads' => array( 'folder' => 'wordpress', 'impact' => 'medium', 'risk' => 'low', 'option_id' => 'block_code_execution_uploads', ), 'prevent-login-info-leakage' => array( 'folder' => 'wordpress', 'option_id' => 'disable_login_feedback', ), 'disable-indexing' => array( 'folder' => 'server', 'option_id' => 'disable_indexing', 'has_deactivation' => true, ), 'rename-admin-user' => array( 'folder' => 'wordpress', 'option_id' => 'rename_admin_user', ), 'display-name-is-login-name' => array( 'folder' => 'wordpress', 'option_id' => 'block_display_is_login', ), 'disable-xmlrpc' => array( 'folder' => 'wordpress', 'option_id' => 'disable_xmlrpc', 'always_include' => false, ), 'vulnerabilities' => array( 'folder' => 'wordpress', 'option_id' => 'enable_vulnerability_scanner', 'admin_only' => true, ), 'class-rsssl-two-factor' => array( 'folder' => 'wordpress/two-fa', 'option_id' => 'login_protection_enabled', 'always_include' => false, ), ) ); /** * Check if this plugin's integration is enabled * @param string $plugin * @param array $details * * @return bool */ if ( ! function_exists('rsssl_is_integration_enabled') ) { function rsssl_is_integration_enabled( $plugin, $details ) { global $rsssl_integrations_list; if ( ! array_key_exists( $plugin, $rsssl_integrations_list ) ) { return false; } if ( $details['always_include'] ) { return true; } //if an integration was just enabled, we keep it enabled until it removes itself from the list. //only for admin users if ( rsssl_is_in_deactivation_list( $plugin ) ) { return true; } $field_id = $details['option_id'] ?? false; if ( ! $field_id ) { return false; } $field_value = $details['option_value'] ?? false; $stored_value = rsssl_get_option( $field_id ); if ( $field_value ) { $invert = false; $condition_met = false; if (strpos($field_value, 'NOT') === 0) { $invert = true; $field_value = str_replace( 'NOT ', '', $field_value); } if ( $stored_value === $field_value ) { $condition_met = true; } if ( $invert ) { $condition_met = !$condition_met; } return $condition_met; } else if ( $stored_value ) { return true; } return false; } } /** * code loaded without privileges to allow integrations between plugins and services, when enabled. */ if ( ! function_exists('rsssl_integrations') ) { function rsssl_integrations() { $safe_mode = defined( 'RSSSL_SAFE_MODE' ) && RSSSL_SAFE_MODE; global $rsssl_integrations_list; foreach ( $rsssl_integrations_list as $plugin => $details ) { $details = wp_parse_args( $details, [ 'option_id' => false, 'always_include' => false, 'folder' => false, 'admin_only' => false, 'is_pro' => false, ] ); if ( $details['admin_only'] && ! rsssl_admin_logged_in() ) { continue; } if ( rsssl_is_integration_enabled( $plugin, $details ) ) { $path = apply_filters( 'rsssl_integrations_path', rsssl_path, $plugin, $details ); $file = $path . 'security/' . $details['folder'] . "/" . $plugin . '.php'; if ( ! file_exists( $file ) && $safe_mode ) { continue; } require_once( $file ); } } } } add_action( 'plugins_loaded', 'rsssl_integrations', 10 ); add_action( 'rsssl_after_saved_fields', 'rsssl_integrations', 20 ); /** * Check if a plugin is on the deactivation list * * @param string $plugin * * @return bool */ if ( ! function_exists('rsssl_is_in_deactivation_list') ) { function rsssl_is_in_deactivation_list( string $plugin ): bool { if ( ! is_admin() || ! is_user_logged_in() ) { return false; } if ( ! is_array( get_option( 'rsssl_deactivate_list', [] ) ) ) { delete_option( 'rsssl_deactivate_list' ); } return in_array( $plugin, get_option( 'rsssl_deactivate_list', [] ) ); } } server/index.php 0000777 00000000040 15251751331 0007676 0 ustar 00 <?php // You don't belong here. server/disable-indexing.php 0000777 00000001065 15251751331 0012005 0 ustar 00 <?php defined( 'ABSPATH' ) or die(); if ( rsssl_is_in_deactivation_list('disable-indexing') ){ rsssl_remove_from_deactivation_list('disable-indexing'); } /** * Disable indexing * * @param array $rules * * @return array [] */ function rsssl_disable_indexing_rules( $rules ) { $rules[] = ['rules' => "\n" . 'Options -Indexes', 'identifier' => 'Options -Indexes']; return $rules; } add_filter('rsssl_htaccess_security_rules', 'rsssl_disable_indexing_rules'); /** * Dropped suggestions for indexing in NGINX as indexing in NGINX is by default disabled. */ class-rsssl-htaccess-file-manager.php 0000777 00000051164 15251751331 0013667 0 ustar 00 <?php /** * class-rsssl-htaccess-file-manager.php * * Responsible for reading, writing and versioning .htaccess * rules via WordPress’s insert_with_markers API. * * @package RSSSL\Pro\Security\WordPress\Firewall\Builders\Rules */ namespace { //Multiple requirements to support different WordPress versions and ensure the filesystem API is available. if ( ! function_exists( 'insert_with_markers' )) { require_once ABSPATH . 'wp-admin/includes/misc.php'; } if ( ! function_exists( 'get_home_path' )) { require_once ABSPATH . 'wp-admin/includes/file.php'; } } namespace RSSSL\Security { /** * Handles low-level .htaccess file operations: * – locating the file, * – reading/writing rules, * – recording history, * – cooperating with WP Rocket. * – will no longer auto-create a missing .htaccess (opt-in via `rsssl_allow_create_htaccess`). */ class RSSSL_Htaccess_File_Manager { /** * Singleton instance. * * @var self|null */ private static ?self $instance = null; /** * Return the shared instance of this class. * * @return self */ public static function get_instance(): self { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } /** * Is used for storing the path to the .htaccess file. */ public string $htaccess_file_path; /** * Constructor. * */ public function __construct() { $this->htaccess_file_path = $this->determineHtaccessFilePath(); $this->registerRocketHooks(); } /** * Determines the path to the .htaccess file based on various conditions. */ private function determineHtaccessFilePath(): string { // Prefer a custom home .htaccess if it exists $homePath = apply_filters('rsssl_home_htaccess_path', get_home_path() . '.htaccess'); if ($this->file_exists($homePath)) { return apply_filters('rsssl_htaccess_file_path', $homePath); } // Otherwise use the default .htaccess in ABSPATH $defaultPath = apply_filters('rsssl_default_htaccess_path', ABSPATH . '.htaccess'); if ($this->file_exists($defaultPath)) { return apply_filters('rsssl_htaccess_file_path', $defaultPath); } // Fallback to WP_CONTENT_DIR/.htaccess (path only; file will not be auto-created) $contentPath = apply_filters('rsssl_wp_content_htaccess_path', WP_CONTENT_DIR . '/.htaccess'); return apply_filters('rsssl_htaccess_file_path', $contentPath); } /** * Registers hooks for WP Rocket activation and deactivation. So we can record the history of changes made by WP Rocket. */ private function registerRocketHooks(): void { // Register hooks for WP Rocket activation and deactivation add_action('rocket_activation', [ $this, 'record_history_from_rocket' ]); add_action('rocket_deactivation', [ $this, 'record_history_from_rocket' ]); } /** * Sets or updates the path to the .htaccess file to be managed. */ public function set_htaccess_file_path(string $htaccess_file_path): void { $this->htaccess_file_path = $htaccess_file_path; } /** * Reads the content of the .htaccess file. */ public function get_htaccess_content():? string { if ( is_file($this->htaccess_file_path) && is_readable($this->htaccess_file_path)) { return file_get_contents($this->htaccess_file_path); } return null; } /** * Writes a rule block to the .htaccess file. */ public function write_rule(array $rule_definition, string $debugTest = 'unknown'): bool { if (! $this->validateRuleDefinition($rule_definition)) { return false; } if (! $this->ensure_htaccess_is_writable()) { return false; } return $this->applyMarkerBlock( $this->extract_name_from_marker($rule_definition['marker']), $this->prepareLines($rule_definition), $debugTest ); } /** * Validates the rule definition before writing. * * @param array $ruleDefinition * @return bool True if valid, false otherwise. */ private function validateRuleDefinition(array $ruleDefinition): bool { if (empty($ruleDefinition['marker'])) { $this->log_error('No marker provided for write_rule.'); return false; } return true; } /** * Prepares the lines to write, inserting a placeholder if needed. * * @param array $ruleDefinition * @return string[] Array of lines to write. */ private function prepareLines(array $ruleDefinition): array { $lines = $ruleDefinition['lines'] ?? []; $isBeingCleared = ! empty($ruleDefinition['clear_rule']); if (empty($lines) && ! $isBeingCleared) { return [ '', '# This feature has not been activated.', '', ]; } return $lines; } /** * Applies a marker block to the .htaccess file, supporting configurable top-priority markers. */ private function applyMarkerBlock(string $markerName, array $lines, string $debugTest = 'unknown'): bool { $oldContent = $this->get_htaccess_content() ?: ''; // Allow certain markers to be forced to the very top of .htaccess (right under any existing top block) $top_markers = apply_filters( 'rsssl_htaccess_top_markers', [ 'Really Simple Auto Prepend File', 'Really Simple Security Redirect' ] ); if ( in_array( $markerName, $top_markers, true ) ) { // first remove any existing marker block with the same name $result = $this->write_top_marker_block( $markerName, $lines ); } else { // WP core will preserve everything outside of your marker $probe = $this->get_htaccess_content(); if ( $this->is_effectively_empty( $probe ) ) { $result = false; } else { // WP core will preserve everything outside of your marker $result = insert_with_markers( $this->htaccess_file_path, $markerName, $lines ); } } if ( $result ) { $newContent = $this->get_htaccess_content() ?: ''; $this->record_history( $oldContent, $newContent, $markerName, $debugTest ); } return $result; } /** * Ensures that the .htaccess file exists and is writable. */ private function ensure_htaccess_is_writable(): bool { $dir = dirname( $this->htaccess_file_path ); // Ensure the directory exists (same as before) if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) { $this->log_error( 'Cannot create directory for .htaccess at: ' . esc_html( $dir ) ); return false; } // Do **not** create a new .htaccess automatically anymore. // This previously led to empty files overwriting existing rewrite rules in some environments. // If a site really wants us to create the file, they must opt in via the filter below. if ( ! is_file( $this->htaccess_file_path ) ) { $allow_create = apply_filters( 'rsssl_allow_create_htaccess', false, $this->htaccess_file_path ); if ( $allow_create ) { if ( @file_put_contents( $this->htaccess_file_path, '', LOCK_EX ) === false ) { $this->log_error( 'Could not create .htaccess file at: ' . esc_html( $this->htaccess_file_path ) ); return false; } else { $this->log_error( 'Created new .htaccess file at: ' . esc_html( $this->htaccess_file_path ) ); } } else { $this->log_error( '.htaccess file does not exist and automatic creation is disabled. Path: ' . esc_html( $this->htaccess_file_path ) ); return false; } } if ( ! is_writable( $this->htaccess_file_path ) ) { $this->log_error( '.htaccess file is not writable at: ' . esc_html( $this->htaccess_file_path ) ); return false; } return true; } /** * Writes a marker block that must live at the very top of .htaccess. * * Used for markers that must run before WordPress rewrite rules – e.g. * - "Really Simple Auto Prepend File" * - "Really Simple Security Redirect" (HTTP→HTTPS redirect) */ private function write_top_marker_block( string $markerName, array $linesToWrite ): bool { // Preserve original content for history $originalHtaccess = $this->get_htaccess_content() ?: ''; // SAFETY: if .htaccess is (effectively) empty or unreadable, do not write our markers if ( $this->is_effectively_empty( $originalHtaccess ) ) { return false; } // we remove the redirect marker block if it exists, so we can write a new one // this is needed because the redirect marker block is not removed by insert_with_markers // We added this function because not on every save we can determine when to remove options when the rule is not present. if ( $markerName !== 'Really Simple Security Redirect' && 'htaccess' !== rsssl_get_option('redirect')) { $originalHtaccess = $this->remove_marker_block( $originalHtaccess, 'Really Simple Security Redirect' ); } $htaccessWithoutMarker = $this->remove_marker_block( $originalHtaccess, $markerName ); if (empty($linesToWrite)) { return $this->save_htaccess_if_changed($originalHtaccess, $htaccessWithoutMarker, $markerName); } $newMarkerBlock = $this->build_marker_block( $markerName, $linesToWrite ); $updatedHtaccess = $this->insert_marker_in_correct_position($htaccessWithoutMarker, $markerName, $newMarkerBlock); $updatedHtaccess = $this->cleanupEmptyLines($updatedHtaccess); @file_put_contents( $this->htaccess_file_path, $updatedHtaccess, LOCK_EX ); $this->record_history( $originalHtaccess, $updatedHtaccess, $markerName ); return true; } /** * Inserts a marker block in the correct position in the .htaccess file. */ private function insert_marker_in_correct_position(string $htaccess, string $markerName, string $markerBlock): string { $autoPrependName = 'Really Simple Auto Prepend File'; if (strcasecmp($markerName, $autoPrependName) === 0) { return $markerBlock . $htaccess; } $escapedAutoPrependName = preg_quote($autoPrependName, '/'); $autoPrependPattern = $this->generate_marker_pattern($autoPrependName); if (preg_match($autoPrependPattern, $htaccess, $match, PREG_OFFSET_CAPTURE)) { $insertPosition = $match[1][1] + strlen($match[1][0]); return substr($htaccess, 0, $insertPosition) . $markerBlock . substr($htaccess, $insertPosition); } return $markerBlock . $htaccess; } /** * Generates a regex pattern to match a marker block in the .htaccess file. * * This pattern matches both # and ### markers, case-insensitive, and captures * the entire block including the BEGIN and END lines. */ public function generate_marker_pattern(string $markerName): string { $escaped = preg_quote($markerName, '/'); //return '/(^#+\s*BEGIN\s+' . $escaped . '[^\n]*\n.*?^#+\s*END\s+' . $escaped . '[^\n]*\n?)/ims'; return '/(^\s*#+\s*BEGIN\s+' . $escaped . '[^\n]*\n.*?^\s*#+\s*END\s+' . $escaped . '[^\n]*\n?)/ims'; } /** * Removes a marker block from the .htaccess file. */ private function remove_marker_block(string $htaccess, string $markerName): string { // Normalize line endings so regex behaves consistently // $htaccess = preg_replace("/\r\n? /", "\n", $htaccess); $htaccess = preg_replace("/\r\n?/", "\n", $htaccess); // Build a single, tolerant pattern matching any number of leading '#', optional trailing text on BEGIN/END lines, // and capturing across multiple lines. $pattern = $this->generate_marker_pattern($markerName); // Apply the replacement and capture match count for debugging $before = $htaccess; $htaccess = preg_replace($pattern, '', $htaccess, -1, $count); return ltrim($htaccess, "\n"); } /** * Saves the .htaccess file if it has changed, and record the history. */ private function save_htaccess_if_changed(string $original, string $modified, string $markerName): bool { if ( $modified === $original ) { return true; } // SAFETY: do not write when the current .htaccess is effectively empty if ( $this->is_effectively_empty( $original ) ) { return false; } $cleaned = $this->cleanupEmptyLines( $modified ); // Avoid writing an empty result if ( $this->is_effectively_empty( $cleaned ) ) { return true; } @file_put_contents( $this->htaccess_file_path, $cleaned, LOCK_EX ); $this->record_history( $original, $cleaned, $markerName ); return true; } private function build_marker_block(string $markerName, array $lines): string { return implode(PHP_EOL, array_merge( ["# BEGIN {$markerName}"], $lines, ["# END {$markerName}"] )) . PHP_EOL; } /** * Checks if a specific marker block exists in the .htaccess file. * * @param array $markers The start and end markers (e.g., ['#BEGIN rule', '#END rule']). * @return bool True if the block exists, false otherwise. */ public function are_markers_present(array $markers): bool { if (count($markers) !== 2) { return false; } $content = $this->get_htaccess_content(); if ($content === null) { return false; } $start_marker_escaped = preg_quote($markers[0], '/'); $end_marker_escaped = preg_quote($markers[1], '/'); return preg_match('/^\s*' . $start_marker_escaped . '.*?^\s*' . $end_marker_escaped . '/ms', $content) === 1; } /** * Extracts a usable name from the BEGIN marker for insert_with_markers. * E.g., "#BEGIN My Rule" becomes "My Rule". */ private function extract_name_from_marker(string $begin_marker): string { // Remove #, BEGIN, Begin, begin and then trim // also remove trailing ### $name = preg_replace( array( '/^#+\s*(BEGIN|Begin|begin)\s*/i', '/\s*#+$/' ), '', $begin_marker ); return trim($name); } /** * Records a change to the .htaccess history. * * @param string $old_content The previous content. * @param string $new_content The new content. */ private function record_history( string $old_content, string $new_content , string $marker = 'unknown', string $debugTest = 'unknown'): void { if ( ! $this->is_htaccess_tracking_enabled() ) { // we remove the option if the constant is not defined. delete_option( 'rsssl_htaccess_history' ); return; } if ( $old_content === $new_content ) { return; } $history = get_option( 'rsssl_htaccess_history', [] ); $history[] = [ 'timestamp' => time(), 'file_path' => $this->htaccess_file_path, 'old_content' => $old_content, 'new_content' => $new_content, 'user_id' => function_exists( 'get_current_user_id' ) ? get_current_user_id() : 0, 'marker' => $marker, // logging the current hook name for debugging purposes 'hook' => current_filter() ?: 'unknown', // logging the current action for debugging purposes 'action' => current_action()? : 'unknown', 'debug_test' => $debugTest, ]; if ( count( $history ) > 20 ) { $history = array_slice( $history, -20 ); } update_option( 'rsssl_htaccess_history', $history, false ); } /** * Clears a specific marker block from the .htaccess file. * * @param string|array $marker The marker name (string) or marker array (['#Begin ...', '#End ...']). * @return bool True on success, false on failure. */ public function clear_rule($marker, string $debugTest = 'unknown'): bool { // Accept either a string (marker name) or an array (markers) if (is_array($marker)) { $begin_marker = $marker[0] ?? ''; } else { $begin_marker = $marker; } $rule_definition = [ 'marker' => $begin_marker, 'lines' => [], 'clear_rule' => true, ]; return $this->write_rule($rule_definition, $debugTest); } /** * Clears a specific marker block from the .htaccess file without using * insert_with_markers. This method directly removes the block using raw * regex matching. This is needed for old markings that had capitalized * Begin and End markers. */ public function clear_legacy_rule(string $marker): bool { $content = $this->get_htaccess_content(); if ($content === null) { return false; } // SAFETY: if the file is effectively empty, do not attempt to rewrite it if ( $this->is_effectively_empty( $content ) ) { return false; } // Match and remove the block with the exact marker name // Use case-insensitive matching for BEGIN/END to handle both legacy and WordPress standard formats $escaped = preg_quote($marker, '/'); $pattern = '/^#+\s*BEGIN\s+' . $escaped . '.*?^#+\s*END\s+' . $escaped . '.*?$/msi'; $new_content = trim(preg_replace($pattern, '', $content)); // Regex error if ($new_content === null) { return false; } // Write the updated content back to the .htaccess file if ( $new_content !== $content && ! $this->is_effectively_empty( $new_content ) ) { return file_put_contents($this->htaccess_file_path, $new_content, LOCK_EX) !== false; } return true; // No changes needed } /** * Records the history of changes made by WP Rocket to the .htaccess file. */ public function record_history_from_rocket(): void { // We get the previous content from the history, if it exists. $history = get_option( 'rsssl_htaccess_history', [] ); $old_content = ''; if ( ! empty( $history ) ) { $last_entry = end( $history ); if ( isset( $last_entry['new_content'] ) ) { $old_content = $last_entry['new_content']; } } $new_content = file_get_contents( $this->htaccess_file_path ); if ( $new_content === false ) { return; } $this->record_history( $old_content, $new_content, 'wp-rocket' ); } /** * Checks if .htaccess history tracking is enabled via constant. * * @since 5.x.x * * @return bool True if .htaccess history tracking is enabled, false otherwise. */ private function is_htaccess_tracking_enabled(): bool { return defined( 'RSSSL_RECORDS_HISTORY_VERSION' ); } /** * Reads the content between a marker block in the .htaccess file and returns it as a string, including the marker lines. */ public function get_rule_content(string $markerName):? string { $content = $this->get_htaccess_content(); if ($content === null) { return null; } // Match both # and ### marker styles, case-insensitive, including the marker lines $escaped = preg_quote($markerName, '/'); $pattern = '/(#+\s*BEGIN\s+' . $escaped . '[^\n]*\n.*?#+\s*END\s+' . $escaped . '[^\n]*\n?)/is'; if (preg_match($pattern, $content, $matches)) { return trim($matches[1]); } return null; } /** * Writes an error message to the error log. */ public function log_error(string $message): void { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( 'RSSSL_Htaccess_File_Manager: ' . $message ); } } /** * Validates the .htaccess file path. If exists, writable and a valid string. */ public function validate_htaccess_file_path(): bool { // Check if the file path is a valid string and not empty if (empty( $this->htaccess_file_path ) ) { return false; } // Check if the file exists and is writable if ( ! is_file( $this->htaccess_file_path ) || ! is_writable( $this->htaccess_file_path ) ) { return false; } return true; } /** * Checks if the .htaccess file exists. */ public function file_exists( string $file_path ): bool { return is_file( $file_path ); } /** * Cleans up extra empty lines in .htaccess content. * * @param string $content The raw .htaccess content. * @return string The content with consecutive blank lines reduced. */ private function cleanupEmptyLines(string $content): string { // Normalize all line endings to "\n" // Collapse three or more consecutive newlines into two $content = preg_replace( array( "/\r\n?/", "/\n{3,}/" ), array( "\n", "\n\n" ), $content ); return $content; } /** * Checks if the given content is effectively empty (only whitespace). */ private function is_effectively_empty( $content ): bool { if ( $content === null || $content === false ) { return true; } return trim( (string) $content ) === ''; } } } includes/check404/class-rsssl-test-404.php 0000777 00000011112 15251751331 0014131 0 ustar 00 <?php namespace RSSSL\Security\Includes\Check404; class Rsssl_Test_404 { // Static instance property public static $instance = null; // Private constructor to prevent direct instantiation private function __construct() { // Immediately check if there are resources to process and handle them $resources = get_option( 'rsssl_404_resources_to_check' ); $found_404_option_value = get_option( 'rsssl_homepage_contains_404_resources', false ); $found_404s = $found_404_option_value === true || $found_404_option_value === "true"; if ( ! empty( $resources ) && ! $found_404s ) { // Trigger chunk processing if resources are pending $this->process_404_resources_chunk(); } $this->fetch_and_check_homepage_resources(); } // Static method to get the single instance of the class public static function get_instance() { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } // Process resources in chunks public function process_404_resources_chunk() { $resources = get_option( 'rsssl_404_resources_to_check' ); if ( empty( $resources ) ) { update_option( 'rsssl_homepage_contains_404_resources', 'false' ); return false; } // Process a chunk of the resources (e.g., 2 at a time) $chunk_size = 2; $resources_chunk = array_splice( $resources, 0, $chunk_size ); $result = $this->process_404_resources( $resources_chunk ); // Update the remaining resources back to the option if ( ! empty( $resources ) ) { update_option( 'rsssl_404_resources_to_check', $resources ); return 'processing'; } else { // All resources have been processed return $result; } } // Function to check homepage and handle 404s public static function homepage_contains_404_resources() { $found_404_option_value = get_option( 'rsssl_homepage_contains_404_resources', false ); if ( $found_404_option_value === true || $found_404_option_value === "true" ) { return true; } $resources = get_option( 'rsssl_404_resources_to_check' ); if ( ! empty( $resources ) ) { // If resources are available to check, process them immediately return self::get_instance()->process_404_resources_chunk(); } } // Function to fetch homepage resources and check for 404 errors public function fetch_and_check_homepage_resources() { if ( get_option('rsssl_homepage_contains_404_resources') ) { return; } $site_url = trailingslashit( home_url() ); $response = wp_remote_get( $site_url ); if ( is_wp_error( $response ) ) { update_option( 'rsssl_homepage_contains_404_resources', false ); return false; } $status_code = wp_remote_retrieve_response_code( $response ); if ( $status_code == 404 ) { update_option( 'rsssl_homepage_contains_404_resources', true ); return true; } // Patterns to match img, script, link tags $body = wp_remote_retrieve_body( $response ); $patterns = array( '/<img[^>]+src=([\'"])?((.*?)\1)/i', '/<script[^>]+src=([\'"])?((.*?)\1)/i', '/<link[^>]+href=([\'"])?((.*?)\1)/i' ); $resources = array(); foreach ( $patterns as $pattern ) { if ( preg_match_all( $pattern, $body, $matches ) ) { foreach ( $matches[2] as $resource_url ) { $resource_url = esc_url_raw( $resource_url ); if ( strpos( $resource_url, $site_url ) !== false ) { $resources[] = $resource_url; } } } } if ( count( $resources ) > 2 ) { update_option( 'rsssl_404_resources_to_check', $resources ); return $this->process_404_resources_chunk(); } else { if ( empty( $resources ) ) { update_option( 'rsssl_homepage_contains_404_resources', 'false' ); return false; } update_option( 'rsssl_404_resources_to_check', $resources ); // Process all resources if fewer than 5 return $this->process_404_resources( $resources ); } } // Function to process a list of resources and check for 404 errors private function process_404_resources( $resources ) { $not_found_resources = array(); foreach ( $resources as $resource_url ) { $resource_response = wp_remote_head( $resource_url ); if ( is_wp_error( $resource_response ) ) { $not_found_resources[] = $resource_url . ' (Error: ' . $resource_response->get_error_message() . ')'; } else { $resource_status = wp_remote_retrieve_response_code( $resource_response ); if ( $resource_status == 404 ) { $not_found_resources[] = $resource_url; } } } if ( empty( $not_found_resources ) ) { update_option( 'rsssl_homepage_contains_404_resources', 'false' ); return false; } else { update_option( 'rsssl_homepage_contains_404_resources', 'true' ); return true; } } } includes/check404/class-rsssl-simple-404-interceptor.php 0000777 00000011441 15251751331 0017004 0 ustar 00 <?php namespace RSSSL\Security\Includes\Check404; class Rsssl_Simple_404_Interceptor { private $attempts = 10; // Default attempts threshold private $time_span = 5; // Time span in seconds (5 seconds) private $option_name = 'rsssl_404_cache'; private $notice_option = 'rsssl_404_notice_shown'; public function __construct() { // Load the 404 test class only if the firewall has been enabled if ( rsssl_get_option('enable_firewall') == '1' ) { add_action( 'admin_init', array( $this, 'maybe_load_class_404_test' ), 20, 4 ); } add_filter( 'rsssl_notices', array( $this, 'show_help_notices' ) ); if ( defined( 'rsssl_pro' ) ) { return; } add_action( 'template_redirect', array( $this, 'detect_404' ) ); } /** * Detect and handle 404 errors. */ public function detect_404(): void { if (is_404()) { if ( get_option( $this->notice_option ) ) { return; } $ip_address = $this->get_ip_address(); $current_time = time(); // Prevent the option from becoming too large $cache = get_option($this->option_name, []); if (!isset($cache[$ip_address])) { $cache[$ip_address] = []; } $cache[$ip_address][] = $current_time; $cache[$ip_address] = $this->clean_up_old_entries($cache[$ip_address]); if (count($cache[$ip_address]) > $this->attempts && !get_option($this->notice_option)) { update_option($this->notice_option, true, false); return; } update_option($this->option_name, $cache, false); } } /** * Cleans up old entries based on the given timestamps. * * This method filters the given timestamps array and only keeps the entries where the difference between the current time * and the timestamp is less than the specified time span. * * @param array $timestamps An array of timestamps. * * @return array The cleaned up timestamps array. */ private function clean_up_old_entries($timestamps): array { $current_time = time(); return array_filter($timestamps, function($timestamp) use ($current_time) { return ($current_time - $timestamp) < $this->time_span; }); } /** * Retrieves the IP address of the client. * * This method checks for the IP address in the following order: * 1. HTTP_CLIENT_IP: Represents the IP address of the client if the client is a shared internet device. * 2. HTTP_X_FORWARDED_FOR: Represents the IP address of the client if the client is accessing the server through a proxy server. * 3. REMOTE_ADDR: Represents the IP address of the client if the client is accessing the server directly. * * @return string The IP address of the client. */ private function get_ip_address(): string { if (!empty($_SERVER['HTTP_CLIENT_IP'])) { return $_SERVER['HTTP_CLIENT_IP']; } if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { return $_SERVER['HTTP_X_FORWARDED_FOR']; } if (!empty($_SERVER['REMOTE_ADDR'])) { return $_SERVER['REMOTE_ADDR']; } return 'UNKNOWN'; } /** * Add a help notice for 404 detection warning. * * @param array $notices The existing notices array. * * @return array Updated notices array with 404 detection warning notice. */ public function show_help_notices(array $notices): array { if (get_option($this->notice_option)) { $message = __('We detected suspected bots triggering large numbers of 404 errors on your site.', 'really-simple-ssl'); $notice = [ 'callback' => '_true_', 'score' => 1, 'show_with_options' => ['enable_404_detection'], 'output' => [ 'true' => [ 'msg' => $message, 'icon' => 'warning', 'type' => 'warning', 'dismissible' => true, 'admin_notice' => false, 'highlight_field_id' => 'enable_firewall', 'plusone' => true, 'url' => 'https://really-simple-ssl.com/suspected-bots-causing-404-errors/', ] ] ]; $notices['404_detection_warning'] = $notice; } return $notices; } /** * @param $field * @param $value * @param $old_value * @param $option_name * * @return void */ public function maybe_load_class_404_test() { if ( ! get_option( 'rsssl_homepage_contains_404_resources' ) ) { Rsssl_Test_404::get_instance(); } } } new Rsssl_Simple_404_Interceptor(); tests.php 0000777 00000032610 15251751331 0006433 0 ustar 00 <?php defined( 'ABSPATH' ) or die(); /** * Check if XML-RPC requests are allowed on this site * POST a request, if the request returns a 200 response code the request is allowed */ function rsssl_xmlrpc_allowed() { $allowed = get_transient( 'rsssl_xmlrpc_allowed' ); if ( !$allowed ) { $allowed = 'allowed'; if ( function_exists( 'curl_init' ) ) { //set a default, in case of time out set_transient( 'rsssl_xmlrpc_allowed', 'no-response', DAY_IN_SECONDS ); $url = site_url() . '/xmlrpc.php'; $ch = curl_init($url); // XML-RPC listMethods call // Valid XML-RPC request $xmlstring = '<?xml version="1.0" encoding="utf-8"?> <methodCall> <methodName>system.listMethods</methodName> <params></params> </methodCall>'; curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded')); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // Post string curl_setopt($ch, CURLOPT_POSTFIELDS, $xmlstring ); curl_setopt($ch, CURLOPT_TIMEOUT, 3); //timeout in seconds curl_exec($ch); $response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($response_code === 200) { $allowed = 'allowed'; } else { $allowed = 'not-allowed'; } } set_transient( 'rsssl_xmlrpc_allowed', $allowed, DAY_IN_SECONDS ); } return $allowed === 'allowed'; } /** * @return bool * Test if HTTP methods are allowed */ function rsssl_http_methods_allowed() { if ( ! rsssl_user_can_manage() ) { return false; } $methods = [ 'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE', 'TRACK', 'PATCH', 'COPY', 'LINK', 'UNLINK', 'PURGE', 'LOCK', 'UNLOCK', 'PROPFIND', 'VIEW', ]; $tested = get_option( 'rsssl_http_methods_allowed' ); #if the option was reset, start couting from 0 if ( !$tested ){ delete_option('rsssl_last_tested_http_method'); } $last_tested = get_option('rsssl_last_tested_http_method', -1); $nr_of_tests_on_batch = 4; if ( !$tested || ( $last_tested < count($methods)-1 ) ) { $tested = get_option( 'rsssl_http_methods_allowed', [] ); $next_test = $last_tested+1; $test_methods = array_slice($methods, $next_test, $nr_of_tests_on_batch, true); update_option('rsssl_last_tested_http_method', $last_tested+$nr_of_tests_on_batch, false); foreach ( $test_methods as $method ) { #set a default, in case a timeout occurs $tested['not-allowed'][] = $method; update_option( 'rsssl_http_methods_allowed', $tested, false ); if ( function_exists( 'curl_init' ) ) { $ch = curl_init(); curl_setopt( $ch, CURLOPT_URL, site_url() ); curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $method ); curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true ); curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); curl_setopt( $ch, CURLOPT_HEADER, true ); curl_setopt( $ch, CURLOPT_NOBODY, true ); curl_setopt( $ch, CURLOPT_VERBOSE, true ); curl_setopt( $ch, CURLOPT_TIMEOUT, 3 ); //timeout in seconds curl_exec( $ch ); #if there are no errors, the request is allowed if ( ! curl_errno( $ch ) ) { //remove the not allowed entry $not_allowed_index = array_search( $method, $tested['not-allowed'], true ); if ( $not_allowed_index !== false ) { unset( $tested['not-allowed'][ $not_allowed_index ] ); } $tested['allowed'][] = $method; } curl_close( $ch ); update_option( 'rsssl_http_methods_allowed', $tested, false ); } } } if ( !empty($tested['allowed'])) { return true; } return false; } /** * @return bool * * Check if DB has default wp_ prefix */ function rsssl_is_default_wp_prefix() { global $wpdb; if ( $wpdb->prefix === 'wp_' ) { return true; } return false; } function rsssl_xmlrpc_enabled(){ return apply_filters('xmlrpc_enabled', true ); } /** * @return bool * * Check if user admin exists */ function rsssl_has_admin_user() { if ( !rsssl_user_can_manage() ) { return false; } //transient is more persistent then wp cache set $count = get_transient('rsssl_admin_user_count'); //get from cache, but not on settings page if ( $count === false || RSSSL()->admin->is_settings_page() ){ //use wp_cache_get to prevent duplicate queries in one pageload $count = wp_cache_get('rsssl_admin_user_count', 'really-simple-ssl'); if ( $count === false ) { global $wpdb; $count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->base_prefix}users WHERE user_login = 'admin'" ); wp_cache_set('rsssl_admin_user_count', $count, 'really-simple-ssl', HOUR_IN_SECONDS ); } set_transient('rsssl_admin_user_count', $count, HOUR_IN_SECONDS); } return $count > 0; } /** * Check if username is valid for use * @return bool */ function rsssl_new_username_valid(): bool { $new_user_login = trim(sanitize_user(rsssl_get_option('new_admin_user_login'))); if ( $new_user_login === 'admin' ) { return false; } $user_exists = get_user_by('login', $new_user_login); if ( $user_exists ) { return false; } return is_string($new_user_login) && strlen($new_user_login)>2; } /** * For backward compatibility we need to wrap this function, as older versions do not have this function (<5.6) * @return bool */ function rsssl_wp_is_application_passwords_available(){ if ( function_exists('wp_is_application_passwords_available') ) { return wp_is_application_passwords_available(); } return false; } /** * Get users where display name is the same as login * * @param bool $return_users * * @return bool | array * */ function rsssl_get_users_where_display_name_is_login( $return_users=false ) { $found_users = []; $users = get_transient('rsssl_admin_users'); if ( !$users ){ $args = array( 'role' => 'administrator', ); $users = get_users( $args ); set_transient('rsssl_admin_users', $users, HOUR_IN_SECONDS); } foreach ( $users as $user ) { if ($user->display_name === $user->user_login) { $found_users[] = $user->user_login; } } // Maybe return users in integration if ( $return_users ) { return $found_users; } if ( count($found_users) > 0 ) { return true; } return false; } /** * Check if debugging in WordPress is enabled * * @return bool */ function rsssl_is_debugging_enabled() { return ( defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG ); } function rsssl_debug_log_value_is_default(){ $value = rsssl_get_debug_log_value(); return (string) $value === 'true'; } /** * Get value of debug_log constant * Please note that for a value 'true', you should check for the string value === 'true' * @return bool|string */ function rsssl_get_debug_log_value(){ if ( !defined('WP_DEBUG_LOG')) { return false; } $wpconfig_path = rsssl_find_wp_config_path(); if ( !$wpconfig_path ) { return false; } $wpconfig = file_get_contents( $wpconfig_path ); // Get WP_DEBUG_LOG declaration $regex = "/^\s*define\([ ]{0,2}[\'|\"]WP_DEBUG_LOG[\'|\"][ ]{0,2},[ ]{0,2}(.*)[ ]{0,2}\);/m"; preg_match( $regex, $wpconfig, $matches ); if ($matches && isset($matches[1]) ){ return trim($matches[1]); } return false; } /** * Check if the debug log file exists in the default location, and if it contains our bogus info * @return bool * */ function rsssl_debug_log_file_exists_in_default_location(){ $default_file = trailingslashit(WP_CONTENT_DIR).'debug.log'; if ( !file_exists($default_file) ) { return false; } //limit max length of string to 500 $content = file_get_contents($default_file, false, null, 0, 500 ); return trim( $content ) !== 'Access denied'; } /** * @return string * Test if code execution is allowed in /uploads folder */ function rsssl_code_execution_allowed() { $code_execution_allowed = get_transient('rsssl_code_execution_allowed_status'); if ( !$code_execution_allowed ) { $upload_dir = wp_get_upload_dir(); //set a default, in case of timeouts $code_execution_allowed = 'not-allowed'; set_transient( 'rsssl_code_execution_allowed_status', $code_execution_allowed, DAY_IN_SECONDS ); $test_file = $upload_dir['basedir'] . '/' . 'code-execution.php'; if ( is_writable($upload_dir['basedir'] ) && ! file_exists( $test_file ) ) { try { copy( rsssl_path . 'security/tests/code-execution.php', $test_file ); } catch (Exception $e) { $code_execution_allowed = 'not-allowed'; } } if ( file_exists( $test_file ) ) { $uploads = wp_upload_dir(); $upload_url = trailingslashit($uploads['baseurl']).'code-execution.php'; $response = wp_remote_get($upload_url); if ( !is_wp_error($response) ) { if ( is_array( $response ) ) { $status = wp_remote_retrieve_response_code( $response ); $web_source = wp_remote_retrieve_body( $response ); } if ( $status != 200 ) { //Could not connect to website $code_execution_allowed = 'not-allowed'; } elseif ( strpos( $web_source, "RSSSL CODE EXECUTION MARKER" ) === false ) { //Mixed content fixer marker not found in the websource $code_execution_allowed = 'not-allowed'; } else { $code_execution_allowed = 'allowed'; } } else { $code_execution_allowed = 'not-allowed'; } } //clean up file again if ( file_exists($test_file) ) { unlink($test_file); } set_transient('rsssl_code_execution_allowed_status', $code_execution_allowed, DAY_IN_SECONDS); } return $code_execution_allowed === 'allowed'; } /** * Test if directory indexing is allowed * We assume allowed if test is not possible due to restrictions. Only an explicity 403 on the response results in "forbidden". * On non htaccess servers, the default is non indexing, so we return forbidden. * * @return bool */ function rsssl_directory_indexing_allowed() { $status = get_transient('rsssl_directory_indexing_status'); if ( !$status ) { if ( !rsssl_uses_htaccess() ) { $status = 'forbidden'; } else { $status = 'allowed'; //set a default, in case of timeouts set_transient( 'rsssl_directory_indexing_status', $status, DAY_IN_SECONDS ); try { $test_folder = 'indexing-test'; $test_dir = trailingslashit(ABSPATH) . $test_folder; if ( ! is_dir( $test_dir ) ) { mkdir( $test_dir, 0755 ); } $response = wp_remote_get(trailingslashit( site_url($test_folder) ) ); if ( is_dir( $test_dir ) ) { rmdir( $test_dir ); } // WP_Error doesn't contain response code, return false if ( !is_wp_error( $response ) ) { $response_code = $response['response']['code']; if ( $response_code === 403 ) { $status = 'forbidden'; } } } catch( Exception $e ) { } } set_transient('rsssl_directory_indexing_status', $status, DAY_IN_SECONDS ); } return $status !== 'forbidden'; } /** * Check if file editing is allowed * @return bool */ function rsssl_file_editing_allowed() { if ( function_exists('wp_is_block_theme') && wp_is_block_theme() ) { return false; } return !defined('DISALLOW_FILE_EDIT' ) || !DISALLOW_FILE_EDIT; } /** * Check if user registration is allowed * @return bool */ function rsssl_user_registration_allowed() { return get_option( 'users_can_register' ); } /** * Check if page source contains WordPress version information * @return bool */ function rsssl_src_contains_wp_version() { $result = get_option('rsssl_wp_version_detected' ); if ( $result===false ) { $result = 'no-response'; update_option( 'rsssl_wp_version_detected', 'no-response', false ); try { $wp_version = get_bloginfo( 'version' ); $web_source = ""; $response = wp_remote_get( home_url() ); if ( ! is_wp_error( $response ) ) { if ( is_array( $response ) ) { $status = wp_remote_retrieve_response_code( $response ); $web_source = wp_remote_retrieve_body( $response ); } if ( $status != 200 ) { $result = 'no-response'; } elseif ( strpos( $web_source, 'ver='.$wp_version ) === false ) { $result = 'not-found'; } else { $result = 'found'; } } update_option( 'rsssl_wp_version_detected', $result, false ); } catch(Exception $e) { update_option( 'rsssl_wp_version_detected', 'no-response', false ); } } return $result==='found'; } /** * Count the number of open hardening features * @return int */ function rsssl_count_open_hardening_features() { $open = 0; $fields = rsssl_fields( false ); // Filter out unused fields $recommended_hardening_fields = array_filter($fields, function($field){ return isset($field['recommended']) && $field['recommended']; }); // Create $hardening_options dynamically based on recommended field IDs $hardening_options = array_map(function($field) { return $field['id']; }, $recommended_hardening_fields); foreach ( $hardening_options as $option ) { // Get the field $field = array_filter( $fields, function ( $f ) use ( $option ) { return $f['id'] === $option; } ); if ( ! empty( $field ) ) { $field = reset( $field ); // Apply the rsssl_disable_fields filter $field = apply_filters( 'rsssl_field', $field, $field['id'] ); // Check if the option is not set to true and the field is not disabled if ( rsssl_get_option( $option ) !== true && ( ! isset( $field['disabled'] ) || $field['disabled'] !== true ) && ( ! isset( $field['value'] ) || $field['value'] !== true ) ) { $open ++; } } } return $open; } function rsssl_has_open_hardening_features() { return rsssl_count_open_hardening_features() > 0; }
| ver. 1.6 |
Github
|
.
| PHP 8.1.34 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка