Authenticated settings change vulnerability in WordPress Quick Page/Post Redirect plugin (unpatched).

Revision: July 09th, 2020

This plugin is not maintained any longer and the vulnerability has never been fixed. Make sure to follow the recommendations below.

Quick Page/Post Redirect, a WordPress plugin with 200,000+ active installations, is prone to an authenticated settings change vulnerability in version 5.1.9 and below.

The plugin is used to redirect a page to a different URL or location.
In the “quick-pagepost-redirect-plugin/page_post_redirect_plugin.php” script, the plugin registers the qppr_save_quick_redirect AJAX action to load the qppr_save_quick_redirect_ajax function:

add_action( 'wp_ajax_qppr_save_quick_redirect', array( $this, 'qppr_save_quick_redirect_ajax' )  );   // register ajax save quick redirect - 5.0.7

The function saves all redirects to the WordPress wp_options table:

function qppr_save_quick_redirect_ajax(){
   check_ajax_referer( 'qppr_ajax_verify', 'security', true );
   $protocols       = apply_filters('qppr_allowed_protocols',array( 'http', 'https', 'ftp', 'ftps', 'mailto', 'news', 'irc', 'gopher', 'nntp', 'feed', 'telnet', 'mms', 'rtsp', 'svn', 'tel', 'fax', 'xmpp'));
   $request       = isset($_POST['request']) && trim($_POST['request']) != '' ? esc_url(str_replace(' ','%20',trim($_POST['request'])), null, 'appip') : '';
   $requestOrig   = isset($_POST['original']) && trim($_POST['original']) != '' ? esc_url(str_replace(' ','%20',trim($_POST['original'])), null, 'appip') : '';
   $destination    = isset($_POST['destination']) && trim($_POST['destination']) != '' ? esc_url(str_replace(' ','%20',trim($_POST['destination'])), null, 'appip') : '';
   $newWin       = isset($_POST['newwin']) && (int) trim($_POST['newwin']) == 1 ? 1 : 0;
   $noFollow     = isset($_POST['nofollow']) && (int) trim($_POST['nofollow']) == 1 ? 1 : 0;
   $updateRow    = isset($_POST['row']) && $_POST['row'] != '' ? (int) str_replace('rowpprdel-','',$_POST['row']) : -1;
   $curRedirects = get_option('quickppr_redirects', array());
   $curMeta      = get_option('quickppr_redirects_meta', array());
   $rkeys        = array_keys($curRedirects);
   $mkeys        = array_keys($curMeta);
...
...
   // now save data back to the db options
   update_option('quickppr_redirects', $curRedirects);
   update_option('quickppr_redirects_meta', $curMeta);
   $this->qppr_try_to_clear_cache_plugins();
   echo 'saved';
   exit;
}

It doesn’t check user capabilities and it is only protected with a security nonce. The nonce and the JavaScript code are populated and echoed by the qppr_admin_scripts function which is loaded by the admin_enqueue_scripts hook, i.e., each time a page loads in the admin backend:

add_action( 'admin_enqueue_scripts' , array( $this, 'qppr_admin_scripts' ) );
...
...
function qppr_admin_scripts($hook){
   if(in_array( $hook, array( 'post-new.php', 'edit.php', 'post.php', 'toplevel_page_redirect-updates', 'quick-redirects_page_redirect-options', 'quick-redirects_page_redirect-summary', 'quick-redirects_page_redirect-faqs', 'quick-redirects_page_redirect-import-export', 'quick-redirects_page_meta_addon' ) ) ){
      $ajax_add_nonce = wp_create_nonce( 'qppr_ajax_verify' );
      $secDeleteNonce = wp_create_nonce( 'qppr_ajax_delete_ALL_verify' );
      $protocols       = apply_filters( 'qppr_allowed_protocols', array( 'http', 'https', 'ftp', 'ftps', 'mailto', 'news', 'irc', 'gopher', 'nntp', 'feed', 'telnet', 'mms', 'rtsp', 'svn', 'tel', 'fax', 'xmpp'));
      wp_enqueue_style( 'qppr_admin_meta_style', plugins_url( '/css/qppr_admin_style.css', __FILE__ ) , null , $this->ppr_curr_version );
      //wp_enqueue_script( 'qppr_admin_meta_script', plugins_url( '/js/qppr_admin_script.js', __FILE__ ) , array('jquery'), $this->ppr_curr_version );
      wp_enqueue_script( 'qppr_admin_meta_script', plugins_url( '/js/qppr_admin_script.min.js', __FILE__ ) , array('jquery'), $this->ppr_curr_version );
      wp_localize_script( 'qppr_admin_meta_script', 'qpprData', array( 'msgAllDeleteConfirm' => __( 'Are you sure you want to PERMANENTLY Delete ALL Redirects and Settings (this cannot be undone)?', 'quick-pagepost-redirect-plugin' ),'msgQuickDeleteConfirm' => __( 'Are you sure you want to PERMANENTLY Delete ALL Quick Redirects?', 'quick-pagepost-redirect-plugin' ), 'msgIndividualDeleteConfirm' => __( 'Are you sure you want to PERMANENTLY Delets ALL Individual Redirects?', 'quick-pagepost-redirect-plugin' ), 'securityDelete' => $secDeleteNonce, 'protocols' => $protocols, 'msgDuplicate' => __( 'Redirect could not be saved as a redirect already exists with the same Request URL.', 'quick-pagepost-redirect-plugin' ) , 'msgDeleteConfirm' => __( 'Are you sure you want to delete this redirect?', 'quick-pagepost-redirect-plugin' ) , 'msgErrorSave' => __( 'Error Saving Redirect\nTry refreshing the page and trying again.', 'quick-pagepost-redirect-plugin' ) , 'msgSelect' => 'select a file', 'msgFileType' => __( 'File type not allowed,\nAllowed file type: *.txt', 'quick-pagepost-redirect-plugin' ) , 'adminURL' => admin_url('admin.php'),'ajaxurl'=> admin_url('admin-ajax.php'), 'security' => $ajax_add_nonce, 'error' => __('Please add at least one redirect before submitting form', 'quick-pagepost-redirect-plugin')));
   }
   return;
}

Here too, it doesn’t check user capabilities but instead restricts the access to the function depending on which page is viewed. The problem is that three of them, ‘post-new.php’, ‘edit.php’ and ‘post.php’, are not restricted to the administrator only because WordPress allows some users with low privileges to access them. An authenticated user such as a contributor could visit the ‘wp-admin/edit.php’ page in the WordPress backend to fetch the leaked security nonce in the source of the HTML page:

The user could then access the qppr_save_quick_redirect_ajax function and use it to create a redirect link that would forward all traffic to an external malicious website. Redirections are performed via the “Location” header:

$ curl https://example.org/ -I
HTTP/1.1 301 Moved Permanently
Date: Mon, 17 Feb 2020 15:44:05 GMT
RedirectType: Quick Page Post Redirect - Quick
X-Redirect-By: WordPress
Location: https://evil.com/

Additional issues

In the same script, the qppr_delete_quick_redirect action, which is used to delete existing redirect links, is too prone to the same vulnerability.

Timeline

We discovered the vulnerability and reported it to the author, unsuccessfully, on February 17th, 2020. The plugin was removed from the wordpress.org repo on February 28th, 2020. Full disclosure on July 09th, 2020.

Recommendations

We recommend to uninstall this plugin as there isn’t any security patch available. If you are using our web application firewall for WordPress, NinjaFirewall WP Edition (free) and NinjaFirewall WP+ Edition (premium), you are protected against this vulnerability.

Stay informed about the latest vulnerabilities