The Wordfence Threat Intelligence team has observed a recent increase in the number of partial vulnerability patches that don’t properly address separate underlying issues. More specifically, we have been seeing an increase in Missing Authorization vulnerabilities that are fixed using tools intended for addressing Cross-Site Request Forgery, which are two independently fixable vulnerability types that should be treated as such.
Wordfence has a dedicated research team which regularly conducts vulnerability research. Our discoveries are added to the Wordfence Intelligence Community Edition Vulnerability database, and are often published on our blog after responsibly disclosing them to the vendors. We have discovered and written about several Missing Authorization and Cross-Site Request Forgery (CSRF) vulnerabilities in the past, both considered independently fixable security issues. In today’s post we want to address the issues that cause these two vulnerabilities to occur, how they relate and how they are different.
Defining the Two Vulnerability Types
Vulnerabilities are generally classified into different types and are often assigned a CWE ID (Common Weakness Enumeration Identifier) to represent that vulnerability type. MITRE, the organization that manages the CWE database, defines Missing Authorization and Cross-Site Request Forgery vulnerabilities as follows:
Missing Authorization: “The product does not perform an authorization check when an actor attempts to access a resource or perform an action.” In WordPress-based software, this is typically done through the use of a capability check utilizing the function current_user_can(), which checks a user’s capability.
Cross-Site Request Forgery: “The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.” In WordPress, this validation is typically done through the use of nonces that are set when an action is initiated and verified by the code that performs the action.
Properly secured WordPress software code should verify that the requester is allowed to perform the request and that they intended to perform the action in the first place. Both are important from a security standpoint and should be addressed independently.
Dissecting a Sample Vulnerability
We have previously discussed what nonces are and how they work, and in addition detailed some ways in which they can be created and validated by software. According to the WordPress Codex, nonces are intended to provide protection against CSRF attacks. The Codex further specifies that nonces “should never be relied on for authentication, authorization, or access control.”
Let’s take a look at how that could be accomplished by examining the following functionality that is intentionally vulnerable:
add_action(‘wp_ajax_nopriv_delete_customers’, ‘my_ajax_function’);
function my_ajax_function (){
delete_all_customers();
}
This function is invoked via a call to wp-admin/admin-ajax.php?action=delete_customers as a nopriv AJAX action. This means anybody can invoke this function and delete all customer records. We’ll assume for the time being that the function delete_all_customers() does not perform any security checks. This demonstrates the presence of two vulnerabilities, a Missing Authorization vulnerability and a Cross-Site Request Forgery vulnerability. The presence of these vulnerabilities would make it possible for anyone to delete customer records using a simple request to the site, or by tricking an administrator into performing the action.
First Step: Addressing the Missing Authorization Vulnerability by Adding a Capability Check
To address the first vulnerability, Missing Authorization, we need to ensure that only individuals with the right privileges can delete customers from our database. Based on best practices, the best way to resolve that is by adding a capability check similar to the one below:
add_action(‘wp_ajax_nopriv_delete_customers’, ‘my_ajax_function’);
function my_ajax_function (){
if ( current_user_can (‘manage_options’) ) {
delete_all_customers();
}
}
This added check makes the assumption that users who have the manage_options capability should be allowed to delete customers. This capability is often used as a proxy for administrative users, and in this case ensures that only users with the manage_options capability, such as administrative users, can delete customers. This effectively patches the missing authorization vulnerability. Note that it is also recommended not to use wp_ajax_nopriv_ hooks for actions that require authorization – changing the hook to add_action(‘wp_ajax_delete_customers’, ‘my_ajax_function’);
is also recommended, as this would prevent unauthenticated attackers from making use of it, though this is not sufficient on its own.
Second Step: Addressing the Cross-Site Request Forgery Vulnerability by Adding Nonce Verification
While the added capability check verifies that a user is authorized to perform the specific action, their intent to perform the action is still not verified, leaving the code vulnerable to Cross-Site Request Forgery. In order for a malicious actor to take advantage of this vulnerability, they would have to successfully trick an administrator of a site to perform an action such as visiting a particular website or clicking a link. This means the vulnerability requires user interaction to be successful.
To exploit a Cross-Site Request Forgery vulnerability as outlined, an attacker who controls attacker[.]com can embed an image as follows into their site, where example[.]com is the targeted vulnerable domain:
<img src=”http://example[.]com/wp-admin/admin-ajax.php?action=delete_customers” />
If the administrator of the site example[.]com visits the site the attacker has prepared, while being authenticated to example[.]com, the visit will execute the AJAX call and result in the deletion of customer data on example[.]com. It is important to understand that the administrator did not perform this action intentionally and, because the application did not verify intent to delete customer data through a nonce check, the request would likely be successful. It is also important to understand that the attacker had to trick an administrator into unintentionally performing this action.
To prevent this Cross-Site Request Forgery Vulnerability from being successful, the code would need a nonce check added which could look like the following code example, with the nonce being generated elsewhere in the code:
add_action(‘wp_ajax_nopriv_delete_customers’, ‘my_ajax_function’);
function my_ajax_function (){
if ( wp_verify_nonce( $_POST[‘my_nonce’], ‘my-nonce’ ) &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp; current_user_can (‘manage_options’) ) {
delete_all_customers();
}
}
What Not to Do: Use Just a Nonce Check
Now that we’ve covered the best way to properly secure code from Missing Authorization and Cross-Site Request Forgery vulnerabilities in WordPress software, let’s look at what not to do. With a nonce check in place of a capability check, this function may look as follows:
add_action(‘wp_ajax_nopriv_delete_customers’, ‘my_ajax_function’);
function my_ajax_function (){
if ( wp_verify_nonce( $_POST[‘my_nonce’], ‘my-nonce’ ) ) {
delete_all_customers();
}
}
If the nonce, my-nonce, is created on a page that is strictly only available to administrators, this check would prevent unauthorized users from executing this function. This is based on the assumption that an attacker would only be able to obtain the nonce if they had administrator privileges on the site, in which case they would also have proper capabilities.
This nonce check would also protect against Cross-Site Request Forgery. A malicious actor who owns attacker[.]com would not be able to create a valid nonce for example[.]com.
Due to the fact that the nonce check appears to block non-administrative users from executing the function, one could argue that this check provides protection against both Cross-Site Request Forgery and Missing Authorization vulnerabilities. However, while the function does appear to be providing protection, it is not providing adequate protection because nonces can easily be disclosed. For that reason, nonce validation should never be relied on as a means of authorization validation.
Why Nonces Are Not a Means of Authorization Control: Nonce Disclosure and Bypasses in the Past
Sometimes plugins and themes disclose nonces due to oversight or developer error. In 2020, we wrote about the PageLayer plugin suffering from nonce disclosure due to the inclusion of a usable nonce in the header section of the source of every page that had previously been edited using the plugin. Simply viewing the generated source revealed the nonce.This gave authenticated visitors access to the plugin’s AJAX actions, which were insufficiently protected using only a nonce check. As a result, subscribers were able to perform actions that were intended for administrators such as updating posts and pages. With proper capability checks in place, exploiting the vulnerability would have been difficult or impossible.
A similar vulnerability was present in the Redirection for Contact Form 7 plugin. This plugin included a nopriv AJAX action that, while not used in the plugin itself, allowed unauthenticated users to generate nonces. More specifically, the unprivileged user could use this AJAX action to create a valid nonce for any action. As a result, an attacker could use the generated nonce to pass any nonce verification in any plugin or theme on the site that relied on nonce checks as a sole means of access control.
A bypass may also be possible in some cases where the nonce is only checked when it is actually set. This was the case in the Ecwid Ecommerce Shopping Cart plugin. The plugin had a nonce check that was performed only when a nonce was provided. If no nonce was included in the request, the check was skipped. Fortunately, this plugin also implemented a capability check and did not rely on the nonce check alone to control access. However, it serves as an important reminder that nonce checks can sometimes be improperly implemented leading to bypasses.
As mentioned before, the WordPress Codex recommends that nonces should never be relied on for authentication, authorization, or access control. Furthermore, you should always “protect your functions using current_user_can(), and always assume nonces can be compromised.”
Conclusion
In today’s post, we looked at some of the differences between verifying authorization and intent and showed why nonce checks alone are not a sufficient means of access control. We hope that developers and security researchers alike will use this information to ensure that they are not only finding and patching more vulnerabilities in the WordPress ecosystem, but are also doing so in the most effective and secure way possible.
If you are a security researcher, you can responsibly disclose your finds to us and obtain a CVE ID and get your name on the Wordfence Intelligence Community Edition leaderboard.
If you believe your site has been compromised, we offer Incident Response services via Wordfence Care. If you need your site cleaned immediately, Wordfence Response offers the same service with 24/7/365 availability and a 1-hour response time. Both of these products include hands-on support in case you need further assistance.
The post Authorization vs. Intent: Why You Should Always Verify Both appeared first on Wordfence.