Latest WooCommerce Version Fixes Security Bypass Utilized by Widely Exploited Vulnerability
In March, the details of a vulnerability that had been fixed in a WordPress plugin that extends the functionality of the plugin WooCommerce were disclosed. The exploitabilty of it should have been limited as it required having access to a value that is only included in WordPress admin pages. WooCommerce claimed to limit access to that to admins. Documentation from the developer states that “By default, WooCommerce blocks non-admin users from entering WP Admin, or seeing the WP Admin bar.” Despite that, the vulnerability was widely exploited.
The explanation for how it could be widely exploited despite that limitation is that the discoverer of the vulnerability disclosed a bypass for that, “WooCommerce customers can access the back-end by adding wc-ajax=1 to the query, e.g., https://example.com/wp-admin/?wc-ajax=1”. The discloser, NinTechNet, provided no explanation of why they publicized that, nor made any mention of contacting the developer about that bypass. It isn’t as if they didn’t know that they were disclosing something that isn’t supposed to be possible, as we had brought that up to them in a situation involving a different vulnerability a couple of weeks before.
The developer of WooCommerce is Automattic, which also sells various security solutions for WordPress websites, so it isn’t unreasonable to think they would have become aware of that bypass at the time. We don’t know if they did at the time, but on May 31, we contacted their security team about it after running into a vulnerability in another WooCommerce extending plugin that was exploitable do to that.
We got a response on June 6 that they were “working on a fix to mitigate this issue” and a week later WooCommerce 7.8.0 was released that did just that. We couldn’t find any reference to the fix in the changelog, though, there were 162 items so we could have missed it.
It getting fixed is very good, that it didn’t get dealt with sooner doesn’t suggest great things about the current state of WordPress security, which runs contrary to how other security providers promote the situation.
Technical Details
For those interested in what is going on under the hood and what the change done was, let’s take a look at the relevant code.
In the file /includes/admin/class-wc-admin.php, the function prevent_admin_access() is registered to run when admin pages are accessed:
27 | add_action( 'admin_init', array( $this, 'prevent_admin_access' ) ); |
Here is how that function looked as of the previous version of WooCommerce:
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | public function prevent_admin_access() { $prevent_access = false; if ( apply_filters( 'woocommerce_disable_admin_bar', true ) && ! wp_doing_ajax() && isset( $_SERVER['SCRIPT_FILENAME'] ) && basename( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_FILENAME'] ) ) ) !== 'admin-post.php' ) { $has_cap = false; $access_caps = array( 'edit_posts', 'manage_woocommerce', 'view_admin_dashboard' ); foreach ( $access_caps as $access_cap ) { if ( current_user_can( $access_cap ) ) { $has_cap = true; break; } } if ( ! $has_cap ) { $prevent_access = true; } } if ( apply_filters( 'woocommerce_prevent_admin_access', $prevent_access ) ) { wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); exit; } } |
Here is the new version:
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | public function prevent_admin_access() { $prevent_access = false; // Do not interfere with admin-post or admin-ajax requests. $exempted_paths = array( 'admin-post.php', 'admin-ajax.php' ); if ( /** * This filter is documented in ../wc-user-functions.php * * @since 3.6.0 */ apply_filters( 'woocommerce_disable_admin_bar', true ) && isset( $_SERVER['SCRIPT_FILENAME'] ) && ! in_array( basename( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_FILENAME'] ) ) ), $exempted_paths, true ) ) { $has_cap = false; $access_caps = array( 'edit_posts', 'manage_woocommerce', 'view_admin_dashboard' ); foreach ( $access_caps as $access_cap ) { if ( current_user_can( $access_cap ) ) { $has_cap = true; break; } } if ( ! $has_cap ) { $prevent_access = true; } } if ( apply_filters( 'woocommerce_prevent_admin_access', $prevent_access ) ) { wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); exit; } } |
The new version no longer aborts doing a redirect if the function wp_doing_ajax() returns true, which it would do if the current request is a WordPress AJAX request. Instead, it checks if the relevant URL for an AJAX request is being accessed by checking if the $_SERVER[‘SCRIPT_FILENAME’] is set to admin-ajax.php.
The reason why that change addresses the issue is that code elsewhere in the plugin causing wp_doing_ajax() to always return true if the GET input “wc-ajax” is not empty:
50 51 | if ( ! empty( $_GET['wc-ajax'] ) ) { wc_maybe_define_constant( 'DOING_AJAX', true ); |