The purpose of this article is to provide information to developers and researchers regarding how vulnerabilities can exist in their plugins or themes and how these vulnerabilities can get patched up in order to increase the safety of the world-wide-web in general.
Note that we will only provide basic information about these vulnerabilities. There is much more in-depth information to it, but this kind of information is widely available on other sites such as PortSwigger which we will link to in each vulnerability type.
Additionally, the examples and remediations only show specific scenarios. Each vulnerability is unique and the remediation for all the different scenarios could greatly differ.
Most common WordPress vulnerabilities
SQL Injection (SQLi)
SQL injection occurs when a user provided input value is not being validated or sanitized properly or not used as part of a prepared statement. SQL injection can also occur when the wrong sanitization function is used, or WordPress functions are used incorrectly.
More information:
- Patchstack Weekly: Patchstack Weekly, Week 06: Preparing for SQL Injection
- OWASP: SQL Injection
- PortSwigger: What is SQL Injection?
Severity
Very high. Affects the integrity and confidentiality of the site. Information could be extracted from the database and malicious information could be inserted into the database depending on the vulnerability.
Example
The examples below assume a user input variable, through a query parameter or form parameter, is being submitted.
$variable = $_REQUEST['id'];
// Vulnerable, user input variable used directly in SQL query.
$wpdb->get_var("SELECT * FROM wp_users WHERE id = " . $variable);
// Vulnerable, esc_sql variables must be enclosed within quotes.
$wpdb->get_var("SELECT * FROM wp_users WHERE id = " . esc_sql($variable));
// Vulnerable, sanitize_text_field does not remove quotes or escape values.
$wpdb->get_var("SELECT * FROM wp_users WHERE id = " . sanitize_text_field($variable));
Remediation
Use the $wpdb->prepare function for any and all interaction with the database that requires user input variables and make sure that you use the placeholder feature (%s, %d, etc) instead of injecting the variables directly into the SQL query. In addition to that, you likely want to validate the user input values you are receiving to make sure that they meet your expectations.
$variable = intval($_REQUEST['id']);
$wpdb->get_var($wpdb->prepare("SELECT * FROM wp_users WHERE id = %d", [$variable]));
Cross-Site Scripting (XSS)
Cross-Site Scripting occurs when user input variables are not being escaped (output) and sanitized (input) properly. This usually happens due to there not being any sanitization and escaping at all or due to a misunderstanding of some of the WordPress functions.
More information:
- Patchstack Weekly: Patchstack Weekly #46: How To Protect WordPress Against Cross-Site Scripting Attacks (XSS)
- OWASP: Cross Site Scripting (XSS)
- PortSwigger: What is cross-site scripting (XSS) and how to prevent it?
Severity
Very high. Depending on where the vulnerable user input variables are displayed, this could either affect the entire site and allow a malicious user to perform a redirect to a malicious site, deface the website or simply execute JavaScript on a very specific page.
Example
The example below assumes a user input variable is saved directly inside of an option, which is then retrieved.
$identifier = get_option('my_identifier');
// Vulnerable, zero output escaping.
echo '<input type="text" name="my_identifier" value="' . $identifier . '">';
// Vulnerable, sanitize_text_field does not escape quotes.
echo '<input type="text" name="my_identifier" value="' . sanitize_text_field($identifier) . '">';
Remediation
WordPress provides a long list of different escape functions you can use to escape your user input variables. Depending on where you output the user provided values, you might have to use different functions. The WordPress link will explain in-depth when and where to use each function.
If your plugin or theme accepts custom HTML provided by a user, you should use the wp_kses function as it allows you to define a whitelist of allowed HTML tags and attributes. However, that gives no guarantee that XSS is not possible.
In the example above, we’d want to use the esc_attr function as it’s a user provided value that is printed into a HTML element’s attribute.
$identifier = get_option('my_identifier');
echo '<input type="text" name="my_identifier" value="' . esc_attr($identifier) . '">';
We also often see cross-site scripting vulnerabilities in plugins that makes a shortcode available. As contributors can also use shortcodes, it may introduce a contributor+ cross-site scripting vulnerability.
For shortcodes the patch is simple as well, just escape the extracted variable. See an example below:
extract( shortcode_atts( array(
'style' => ''
), $params ) );
// Vulnerable:
$html = '<div style="' . $style . '" class="my class">';
// Should be:
$html = '<div style="' . esc_attr($style) . '" class="my class">';
// Some other code here to generate the HTML...
return $html;
Broken Access Control
Broken access control vulnerabilities exist when the authorization and/or authentication of the user is not properly checked. We have seen this very often in actions registered under the following hooks: wp_ajax_*, admin_action_*, admin_post_*, admin_init, register_rest_route.
These hooks do not check the authorization and authentication of the user by default. For register_rest_route that would be the case when the permission callback function always returns true.
More information:
- Patchstack Weekly: Patchstack Weekly, Week 45: Authentication vs Authorization
- Patchstack Weekly: Patchstack Weekly: Secure AJAX Endpoints & WordPress Vulnerabilities
- OWASP: A01 Broken Access Control – OWASP Top 10:2021
- PortSwigger: Access control vulnerabilities and privilege escalation
Severity
Very high. We have seen a significant number of vulnerabilities that exist because of a missing authorization check which often leads to a plugin settings change vulnerability. This can affect the integrity, confidentiality and availability of your site.
Example
This example registers a WordPress AJAX action. Since it’s registered under wp_ajax_*, it requires at least subscriber+ privileges.
// For this example, we assume there is no nonce token check.
add_action('wp_ajax_update_settings', function(){
update_option("my_settings", $_POST);
});
Remediation
WordPress has several functions to determine the authorization of the user. For example, current_user_can and user_can. These can be easily implemented to make sure that unauthorized users cannot perform higher privileged actions. It is also important to check a nonce token in all actions, more information about that in the Cross-Site Request Forgery section.
add_action('wp_ajax_update_settings', function() {
if (!current_user_can('manage_options') || !wp_verify_nonce('action', 'action')) {
exit;
}
update_option("my_settings", [
'setting1' => esc_html($_POST['setting1'])
]);
});
Cross-Site Request Forgery (CSRF)
Cross-Site Request Forgery occurs when a nonce token is not being validated while an authorized action is being performed. For example, if you have a WordPress AJAX action that only validates the authorization but not a nonce token, it is possible to force the higher privileged user to perform that action with arbitrary values by either tricking them into visiting a malicious site with the CSRF payload or by executing XSS on the same site.
More information:
- Patchstack Weekly: Patchstack Weekly: WordPress Vulnerabilities & Cross-Site Request Forgery
- OWASP: Cross Site Request Forgery (CSRF)
- PortSwigger: What is CSRF (Cross-site request forgery)?
Severity
Varies. CSRF is rarely exploited in the wild as it depends on the higher privileged user and might require some level of social engineering as well. In addition, the action that can be executed through CSRF needs to be executing a meaningful action as well.
Example
The example below which although validates the authorization of the user, does not validate a nonce token.
add_action('wp_ajax_update_settings', function() {
if (!current_user_can('manage_options')) {
exit;
}
update_option("my_settings", [
'setting1' => esc_html($_POST['setting1'])
]);
});
Remediation
WordPress provides you with a few functions you can use to validate a nonce token. First, you have to make sure that the nonce token is available to the user. You can generate a nonce token for the frontend using the wp_create_nonce function.
With the passed nonce value, you can use the wp_verify_nonce function in an if statement or use the check_admin_referer function outside of an if statement as it automatically exits the script if no nonce token could be validated.
add_action('wp_ajax_update_settings', function() {
if (!current_user_can('manage_options') || !wp_verify_nonce('action', 'action')) {
exit;
}
update_option("my_settings", [
'setting1' => esc_html($_POST['setting1'])
]);
});
Note: make sure that the nonce token check is correctly implemented. We often see nonce token validation implementations which can be bypassed due to mistakes in the way it is implemented. An example is shown below. This is still vulnerable to CSRF because if we simply don’t pass the action POST parameter in the CSRF payload, the nonce token is not validated either.
add_action('wp_ajax_update_settings', function() {
if (!current_user_can('manage_options')) {
exit;
}
// Should be: if (!isset($_POST['action']) || !wp_verify_nonce('action', 'action')) {
if (isset($_POST['action']) && !wp_verify_nonce('action', 'action')) {
exit;
}
update_option("my_settings", [
'setting1' => esc_html($_POST['setting1'])
]);
});
Server-Side Request Forgery (SSRF)
Server-Side Request Forgery occurs when a HTTP request is sent where the URL can be specified by the user thus allowing them to cause the server to send a request to any URL they desire.
If there are local services running which are not accessible by the public, this vulnerability can be used to get access to those local services.
More information:
- Patchstack Weekly: Patchstack Weekly #33: What is Server Side Request Forgery (SSRF)?
- OWASP: Server Side Request Forgery
- PortSwigger: What is SSRF (Server-side request forgery)?
Severity
Varies. This completely depends on what is running on a private environment which can be accessed through a SSRF attack. For the usual WordPress environment this will have close to no impact on the security state. In addition, this also depends on whether or not the result of the HTTP request is returned to the user or not, because otherwise it would be a blind SSRF issue which tend to be even harder to exploit successfully.
Example
wp_remote_get($_GET['url']);
Remediation
Validate the URL before it’s used in a HTTP request. You can use the wp_safe_remote_get function instead of wp_remote_get or similar functions such as file_get_contents or usage of the cURL library.
Directory Traversal
Directory traversal, sometimes also known as path traversal or arbitrary file read, occurs when a file can be read from the filesystem and its contents are returned back to the user. This should not be confused with Local File Inclusion, which is (in most scenarios) used to execute a file on the website under the context of the webserver/PHP.
More information:
- OWASP: Path Traversal
- PortSwigger: What is directory traversal, and how to prevent it?
Severity
Medium to very high. The severity depends on what kind of information is stored on the filesystem of the site and also depends on the setup of the site. For example, if for some reason there is a publicly accessible PHPMyAdmin installation then the wp-config.php file could be read after which the database connection information can be used to connect to the PHPMyAdmin environment.
Example
echo file_get_contents($_GET['file']);
Remediation
Validate the input parameter before it’s used in any function that reads a file. You could match it against a list of acceptable values, verify that it only contains letters using ctype_alpha, or remove all slashes and dots. However, keep in mind the fix entirely depends on how this function call is implemented.
Local File Inclusion (LFI)
Local File Inclusion occurs when an arbitrary file can be included and executed under the context of the webserver/PHP. This can be especially dangerous if users can upload a file, regardless of the file extension. If a file such as image.png is included using PHP functions such as include or require, any PHP code inside of this file will still get executed.
Severity
Medium to very high. The severity depends on what files are available on the website that can be included and if the user can or cannot upload their own files.
Example
include $_GET['test'];
require_once './some/folder/' . $_GET['test'];
Remediation
Validate the input parameter before it’s used in any function that includes a file. You could match it against a list of acceptable values, verify that it only contains letters using ctype_alpha, or remove all slashes and dots. However, keep in mind the fix entirely depends on how this function call is implemented.
Remote File Inclusion (RFI)
Remote File Inclusion occurs when a malicious user can cause the webserver/PHP to load a remote file and execute it under the context of the webserver/PHP. The most common RFI vulnerability typically requires PHPs allow_url_include to be set to 1.
Severity
Very high. This could cause a complete loss of integrity, confidentiality and availability.
Example
include $_GET['test'];
Remediation
You should never implement the ability to load a remote URL this way. If you must, you should make sure that the user supplied value is part of a whitelist or strict pattern.
Remote Code Execution (RCE)
Remote Code Execution occurs when a user supplied value is executed in a PHP function that executes a shell command. Some of these functions include shell_exec, exec, popen, system, passthru and proc_open.
More information:
- Patchstack: Patching Remote Code Execution in the ‘member-hero’ Plugin
- OWASP: Command Injection
- PortSwigger: What is OS command injection, and how to prevent it?
Severity
Very high. Although this also depends on the configuration of the hosting environment and PHP, this could still cause a lot of damage. Someone could, under the right conditions, upload backdoors (wget/curl command) or reverse shell (netcat).
Example
shell_exec('imgoptimize ' . $_GET['cmd']);
Remediation
The user input values must be checked to only contain allowed values. If you need to escape arguments passed to a binary, consider using escapeshellarg. However, keep in mind that the remediation entirely depends on how the user input value is injected into the command.
CSV Injection
CSV Injection occurs when user supplied values are directly inserted into an exported CSV file. When this CSV file is then opened in an application such as Windows Excel, the maliciously injected values from the user could contain a formula that executes a command instead.
More information:
- Patchstack Weekly: Patchstack Weekly #30: What is CSV Injection?
- OWASP: CSV Injection
Severity
Low to medium. This would also require the generated CSV file to be opened inside of an application that allows these kinds of functions to be executed. Exploitation would require several steps and potentially social engineering too to get the higher privileged user to export/download the CSV file and then open it in the application.
Remediation
The remediation is pretty straightforward. Certain characters that are related to functions should be escaped with a quote. An example can be seen below. You pass the CSV row (the array of values for one row, typically then passed to fputcsv) to this function and it will escape the appropriate characters.
function get_encoded_row( $row ) {
$result = [];
foreach ( $row as $key => $value ) {
$encoded_value = $value;
if ( in_array( substr( (string) $value, 0, 1 ), [ '=', '-', '+', '@', "\t", "\r" ], true ) ) {
$encoded_value = "'" . $value;
}
$result[ $key ] = $encoded_value;
}
return $result;
}
Data Exposure
Data Exposure vulnerabilities occur when lower privileged users can trigger a certain action or hook that exposes (sensitive) information about the website or its users. We typically see these kinds of vulnerabilities in plugins that expose WooCommerce order information or full addresses.
More information:
- OWASP: OWASP Top Ten 2017 | A3:2017-Sensitive Data Exposure
- PortSwigger: Information disclosure vulnerabilities
Severity
Low to medium. This typically only affects the confidentiality of the website.
Remediation
This vulnerability is usually present due to the lack of authorization or authentication, or IDOR. The method or action that allows someone to pull up the information needs to have the proper authorization and authentication checks implemented to prevent the data from being leaked.
WordPress has several functions to determine the authorization of the user. For example, current_user_can and user_can.
Insecure Direct Object Reference (IDOR)
Insecure Direct Object Reference vulnerabilities occur when a certain endpoint or action does not properly validate that the user has the appropriate privileges to access the requested resource.
For example, if there is a page that views orders by order id in the URL using the ?order_id parameter, then lower privileged users should not be able to simply change that number to see orders of other customers.
More information:
- OWASP: Insecure Direct Object Reference Prevention
- PortSwigger: Insecure direct object references (IDOR)
Severity
Low to medium. This typically only affects the confidentiality of the website.
Remediation
The user id bound to the resource should be matched with the current logged in user. For example, if there is an order with user_id 5 then someone who is logged in under a user with user_id 6 should not be able to view this order.
Open Redirect
Open Redirect occurs when the site performs a redirect based on a user supplied value that is not being validated. When a redirect is performed based on a full URL parameter, it should be made sure that this URL is not pointing to an illegitimate endpoint.
More information:
- Patchstack Weekly: Patchstack Weekly #45: What Is an Open Redirect Bug (and Why It’s Dangerous)?
- OWASP: Unvalidated Redirects and Forwards
- MITRE: CWE – CWE-601: URL Redirection to Untrusted Site (‘Open Redirect’) (4.9) (mitre.org)
- PortSwigger: Open redirection (reflected)
Severity
Low to medium. This is rarely exploited and typically goes hand-in-hand with social engineering.
Example
header('Location: ' . $_GET['url']);
// or
wp_redirect($_GET['url']);
Remediation
Use the wp_safe_redirect function to redirect a user. If the redirect URL is anything other than the site itself, consider using the allowed_redirect_hosts filter to add more whitelisted hosts.
Privilege Escalation
Privilege Escalation occurs when a lower privileged or unauthenticated user can perform an action that escalates their current privilege to something higher. We usually see privilege escalation vulnerabilities that allow unauthenticated users to login as administrator.
More information:
Severity
Very high. If the user can login as an administrator, they can take full control of the website which will impact the integrity, confidentiality and availability of the website.
Example
We have seen this vulnerability often in plugins that provide some kind of alternative login functionality, such as login through social media. If they do not validate the login through social media properly, it is possible to bypass any authentication process and login as any user.
Typically, this is possible due to functions such as wp_set_auth_cookie or wp_set_current_user.
Remediation
As each scenario for this vulnerability is very different, it is difficult to provide one single remediation. It is important to properly validate the authentication before a call is made to WordPress functions that allow you to login as a given user.
Race Condition
Race Condition vulnerabilities can occur in functions that typically execute a transaction which users should only be able to execute once. We have seen this often in voting or rating functions.
By sending a large amount of HTTP requests at the same time, it is possible for the PHP process and database server to not be aware of the fact that the user has already processed the request and thus you could force one action to be executed multiple times.
More information:
Severity
Low to medium. Although this kind of vulnerability is hard to patch from a firewall’s perspective, it is rarely exploited.
Remediation
For race conditions inside of SQL related functions you could use transaction locking but for WordPress this could be difficult to implement.
An alternative solution is to implement mutex locks which relies on the filesystem. An example of an implementation of mutex locks can be found here.
Arbitrary File Upload
Arbitrary File Upload occurs when a file uploaded by a user does not have its file extension checked properly. This in turn could allow a .php file to be uploaded which could result in a full compromise of the website.
More information:
- Patchstack Weekly: Patchstack Weekly, Week 12: Secure WordPress File Uploads
- OWASP: Unrestricted File Upload
- PortSwigger: File uploads
Severity
Very high. This can impact the integrity, confidentiality and availability of the website.
Example
move_uploaded_file($_FILES['file']['tmp_name'], '/wp-content/uploads/' . $_FILES['file']['name']);
Remediation
WordPress has its own file uploading functionality so native PHP functions should never be used to handle the upload of a file. Use the wp_handle_upload function to upload files. This function will automatically check the file extension and mime type of the uploaded file.
Arbitrary File Download
Arbitrary File Download occurs when a file is downloaded from the server with a user supplied value. For example, there might be an export function that after clicking export redirects the user to a URL that prompts the download. If this function accepts a query parameter that points to the file, it could be possible to change folder to download any file you desire.
More Information:
Severity
Medium to very high. The severity depends on what kind of information is stored on the filesystem of the site and also depends on the setup of the site. For example, if for some reason there is a publicly accessible PHPMyAdmin installation then the wp-config.php file could be downloaded after which the database connection information can be used to connect to the PHPMyAdmin environment.
Example
$file = $_GET['filename'];
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($file).'"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
Remediation
Validate the input parameter before it’s used in any function that reads a file. You could match it against a list of acceptable values, verify that it only contains letters using ctype_alpha, or remove all slashes and dots. However, keep in mind the fix entirely depends on how this function call is implemented.
Arbitrary File Deletion
Arbitrary File Deletion vulnerabilities typically occur when a user supplied value is being passed to PHP’s unlink function. This function will delete a file and so by passing a path to a file in a different directory, we could delete any file we want.
Severity
Medium to very high. This depends on how the removal of the file is implemented. If the user supplied value is directly used in the unlink function, then it would be a very high severity vulnerability.
Example
unlink('/wp-content/uploads/' . $_GET['file']);
Remediation
Validate the input parameter before it’s used in any function that reads a file. You could match it against a list of acceptable values, verify that it only contains letters using ctype_alpha, or remove all slashes and dots. However, keep in mind the fix entirely depends on how this function call is implemented.
You can also consider using the wp_delete_file_from_directory function.
Denial of Service (DoS)
Denial of Service occurs when a function accepts a user supplied value that can have a significant impact on the resource usage of the server. For example, if a function loops through a user supplied value, then the user could supply a very long string to cause the loop to iterate thousands of times.
Another example of a denial of service is when access to a specific page or all pages can be denied due to a malicious user triggering a certain condition.
More information:
- OWASP: Denial of Service
Severity
Low to medium. This depends on what exactly the vulnerable function does and how much load it actually causes on the server. Worst case scenario, it affects the availability of the website.
Example
By suppling the POST nums value with a very long string such as 1,2,3,1,2,3,1… and so on, we can execute a large number of SQL queries in one single HTTP request. This could cause a lot of load on the database server. Cheap hosting providers might have strict limitations in place which could result in the temporary termination of your website.
$t = explode(',', $_POST['nums']);
foreach ($t as $value) {
$var = $wpdb->get_var($wpdb-prepare("SELECT * FROM wp_users WHERE id = %d"), [$value]);
}
Another example is when the IP address of a user is taken from different IP address headers, such as X-Forwarded-For, and is then used in another function that could result in the inability to view a certain page.
Remediation
The remediation depends on how the vulnerability exists in the first place, so this can differ greatly from plugin to plugin. In the case of the example above, you’d want to limit the number of values of $t before you enter the loop.
$t = explode(',', $_POST['nums']);
if (count($t) > 50) {
exit;
}
foreach ($t as $value) {
$var = $wpdb->get_var($wpdb-prepare("SELECT * FROM wp_users WHERE id = %d"), [$value]);
}
When it comes to IP address spoofing, you should only use the $_SERVER[‘REMOTE_ADDR’] variable for IP addresses. Only the administrator should be able to define which other IP address header should be accepted, and this should come with the warning that it could be spoofed on misconfigured environments.
Type Juggling / Loose String Comparison
Type Juggling or Loose String Comparison typically occurs when a plugin accepts a JSON payload, decodes this payload into an array, and then compares the user provided values with other values using == or !=.
More information:
- Patchstack Weekly: Patchstack Weekly #47: What Is Type Juggling in PHP?
- OWASP: PHPMagicTricks-TypeJuggling.pdf (owasp.org)
Severity
Low to very high. This entirely depends on what the endpoint that accepts the JSON payload uses it for. If, for example, a JSON payload is expected on a secret API endpoint that matches a secret key to an internal key using ==, then this check can be bypassed and results in a high severity vulnerability.
Example
In the example below, we can bypass the secret key check by simply passing a “key” property in our $_POST[‘data’] JSON payload that is set to a true boolean value. Since in PHP true == ‘string’, this check will be bypassed.
$data = json_decode($_POST['data'], true);
$secret_key = get_option('my_secret_key');
if ($data['key'] != $secret_key) {
exit;
}
Remediation
Compare values using === or !==. This will match against both type and value.
PHP Object Injection / Insecure Deserialization
PHP Object Injection or Insecure Deserialization happens when a user supplied value is passed to the unserialize PHP function or maybe_unserialize WordPress function. When this user supplied value is an object, PHP will automatically attempt to call the __unserialize() or __wakeup() methods, if one exists. This would require a POP chain to be present, or a PHP class that is loaded which has this function present which contains code that could be exploited.
More information:
- Patchstack Weekly: PHP Object Injection aka Insecure Deserialize
- Patchstack: A “New” Bug – PHP Object Injection via Insecure Instantiation
- OWASP: PHP Object Injection
- PortSwigger: Insecure deserialization
Severity
Varies. This depends on any available POP chains on the website.
Examples
unserialize($_GET['myvalue']);
Remediation
Do not store serialized PHP object strings in the browser. If the data must be stored with the browser, then you should convert the object or its data to a data structure like JSON using json_encode() and reconstruct the object later using the same data and json_decode().