Vulnerabilities fixed in WordPress Controlled Admin Access plugin.

Improper input validation in the WordPress Controlled Admin Access plugin (8,000+ active installations) affecting version 1.5.5 and below could lead to privilege escalation.

Improper Input Validation

Controlled Admin Access is a plugin used to give a temporary and limited admin access to the WordPress dashboard. It doesn’t remove capabilities but instead relies on a user-defined blacklist of URI or various parameters, all user input, to restrict access to certain pages or sub-menus. By default, access to the plugin configuration pages, e.g., wp-admin/users.php?page=controlled_admin_access, is restricted too:

In the “controlled-admin-access/admin/class-controlled-admin-access-admin.php” script, the plugin compares the value of the $_GET['page'] input to the list of restricted pages stored in the $not_allowed_pages array:

if (isset($_GET['page'])) {
   if(in_array($_GET['page'], $not_allowed_pages)){
      wp_die(__('You do not have sufficient permissions to access this page.'),'controlled-admin-access');

Sub-menus are loaded by calling the WordPress add_submenu_page function:

public function register_my_custom_submenu_pages(){
      __('Controlled Admin Access', 'controlled-admin-access'),
      __('Controlled Admin Access', 'controlled-admin-access'),
      array($this, 'controlled_admin_access_page') );

The fifth parameter of the function, controlled_admin_access, is the value assigned to the $_GET['page'] input and is called the menu_slug.
The add_submenu_page function is found in the wp-admin/includes/plugin.php script:

function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $function = '', $position = null ) {
   global $submenu, $menu, $_wp_real_parent_file, $_wp_submenu_nopriv,
      $_registered_pages, $_parent_pages;

   $menu_slug   = plugin_basename( $menu_slug );

We can see that it immediately calls the plugin_basename function with the $menu_slug as a parameter. The function is found in the wp-includes/plugin.php:

function plugin_basename( $file ) {
   global $wp_plugin_paths;

   // $wp_plugin_paths contains normalized paths.
   $file = wp_normalize_path( $file );

   arsort( $wp_plugin_paths );
   foreach ( $wp_plugin_paths as $dir => $realdir ) {
      if ( strpos( $file, $realdir ) === 0 ) {
         $file = $dir . substr( $file, strlen( $realdir ) );

   $plugin_dir    = wp_normalize_path( WP_PLUGIN_DIR );
   $mu_plugin_dir = wp_normalize_path( WPMU_PLUGIN_DIR );

   // Get relative path from plugins directory.
   $file = preg_replace( '#^' . preg_quote( $plugin_dir, '#' ) . '/|^' . preg_quote( $mu_plugin_dir, '#' ) . '/#', '', $file );
   $file = trim( $file, '/' );
   return $file;

Interestingly, the function can potentially transform the menu_slug lines 690, 695, 703 and 704.
Therefore, if we can access any sub-menu page in the WordPress dashboard by using the ?page=menu_slug format, we can also access it by inserting one or more leading or trailing slashes such as ?page=/menu_slug or ?page=///////menu_slug/////////, or even prepend the full path to the plugins folder such as ?page=/home/foo/html/wp-content/plugins/menu_slug. WordPress will clean up the mess and will load the requested sub-menu anyway.
Using such trick, a user with restricted access to the dashboard can access the Controlled Admin Access configuration page:

The user can submit the above form and create a new administrator with full, unrestricted access to the blog.

Additional Issues

The plugin restricts access to some pages based on the query string without decoding it. A user can access those pages simply by encoding one or more characters in the query string (%xx).


Update immediately if you have version 1.5.5 or below installed.
Note that if you are using our NinjaFirewall plugin, you can restrict its access to specific admin users without the need of a third-party plugin (see Restricting access to NinjaFirewall (WP Edition) settings).


The vulnerability was reported to the authors on March 26. A new version 1.5.6 was released on March 28.

Stay informed about the latest vulnerabilities