Cross-site scripting (XSS) vulnerabilities occupy one of the first places in terms of frequency among the vulnerabilities found in WordPress plugins. These vulnerabilities occur when data from a user is not sufficiently cleaned before being displayed on site pages, which allows attackers to inject malicious code such as JavaScript and execute it in visitors’ browsers. XSS attacks can lead to theft of user data, hijacking of sessions, modification of page content, and other types of malicious activity.
The problem of cross-site scripting is especially relevant for WordPress, where the use of third-party plugins is widespread. Each plugin represents an additional potential attack vector, and many developers face difficulties in ensuring an adequate level of data I/O security. Insufficient protection and clearing of parameters can lead to reflected or stored XSS attacks, which threatens the security of users and the reputation of the website.
Types of Cross-Site Scripting attacks and protection methods
There are several types of Cross-Site Scripting attacks, the most common are Stored XSS and Reflected XSS
Stored XSS
With Stored XSS, malicious code is stored on the server, for example, in a database, and is displayed to every user who opens a page with this content. This is especially dangerous because the code is executed automatically when the page is viewed.
Let’s look at examples of Vulnerable code:
if ( isset( $_POST['comment'] ) ) {
$comment = $_POST['comment'];
$wpdb->insert( 'wp_comments', array( 'comment_content' => $comment ) );
}
$comments = $wpdb->get_results( "SELECT comment_content FROM wp_comments" );
foreach ( $comments as $comment ) {
echo $comment->comment_content;
}
If an attacker adds a comment <script>alert(‘XSS’)</script>, then this code will be saved in the database and executed every time the page with comments is loaded.
Fixed code:
if ( isset( $_POST['comment'] ) ) {
$comment = sanitize_text_field( $_POST['comment'] );
$wpdb->insert( 'wp_comments', array( 'comment_content' => $comment ) );
}
$comments = $wpdb->get_results( "SELECT comment_content FROM wp_comments" );
foreach ( $comments as $comment ) {
echo esc_html( $comment->comment_content );
}
The esc_html() function is used to safely output the contents of each comment in the browser by escaping any HTML objects. In particular, the esc_html() function converts characters such as <, > and & to their HTML equivalents (<lt;, > and &). This prevents the execution of any embedded HTML or JavaScript code, which is crucial to protect against cross-site scripting (XSS) attacks.
Let’s take the example of the vulnerability CVE-2024–9021(link:https://research.cleantalk.org/cve-2024-9021/) :
The plugin is vulnerable to cross-site scripting via the user name field in all versions up to and including 4.23.0 due to insufficient cleaning of input data and escaping of output data. This allows unauthorized attackers to inject arbitrary web scripts into pages that will be executed every time a user visits the embedded page.
The vulnerability was found in Custom Fields for the user, where an XSS payload can be saved. As a result of insufficient filtration, the payload was being processed.
The vulnerable line in the plugin initially looked like this:
The values from the $custom_fields array are combined into a delimited string and returned without additional processing. If $custom_fields contains data entered by the user, this can lead to XSS, since the code can be output to HTML directly.
Fixed code: return htmlspecialchars( implode( ', ', $custom_fields ) );
The htmlspecialchars() function processes the implode() result by converting special characters such as <, >, &, and ” into HTML entities (<lt;, >, &, and " respectively). This allows the data to be displayed safely, since the browser treats special characters as text, and not as HTML tags or JavaScript code.
Reflected XSS
Reflected XSS (Reflected Cross-site Scripting) is a type of vulnerability in which a malicious script is embedded in a web page and immediately executed in the user’s browser. This vulnerability occurs when data provided by a user is returned in a web server response without proper verification and screening.
In WordPress, this can happen, for example, when user input (such as URL parameters, form fields, or cookies) is displayed on a web page without proper processing. A malicious script transmitted through such data can be executed in the user’s browser if they visit a page containing this input.
In WordPress, this can happen, for example, when user input (such as URL parameters, form fields, or cookies) is displayed on a web page without proper processing. A malicious script transmitted through such data can be executed in the user’s browser if they visit a page containing this input.
Vulnerable code:
if ( isset( $_GET['search'] ) ) {
$search_query = $_GET['search'];
echo "You searched for: " . $search_query;
}
An attacker can pass malicious code in the search URL parameter, for example:
https://example.com/?search=<script>alert('XSS')</script>
This code will be executed in the browser if it is displayed on the page, since the data from $_GET[‘search’] is not escaped before output.
if ( isset( $_GET['search'] ) ) {
$search_query = htmlspecialchars( $_GET['search'], ENT_QUOTES, 'UTF-8' );
echo " You searched for: " . $search_query;
}
The second example of vulnerable code:
if ( isset( $_GET['username'] ) ) {
$username = $_GET['username'];
echo '';
}
An attacker can use the following payload:
https://example.com/?username="; onfocus="alert('XSS')
This will result in JavaScript execution, and the input field gets focus, due to the fact that the raw value will be displayed directly in the value attribute.
Fixed code using the esc_attr() function:
if ( isset( $_GET['username'] ) ) {
$username = $_GET['username'];
echo '';
}
The esc_attr() function escapes special characters in HTML attributes (for example, “and =”), preventing the execution of malicious code entered using attributes.
Using the example of vulnerability CVE-2024–7313(https://research.cleantalk.org/cve-2024-7313/ ) :
A Reflected XSS vulnerability was discovered on one of the plugins in the URL field of the nav_sub parameter.
The values of $nav and $subNav are directly extracted from the $this->action_data array and assigned without any cleanup. Constants::NAV_SUB_ID were obtained from user input (for example, from a GET request), an attacker could have passed malicious JavaScript code there.
Fixed code:
In this version of the code, the sanitize_key() function has been added when assigning values to $nav and $subNav. This function clears the string, leaving only safe characters (letters, numbers, underscores and hyphens), which prevents the introduction of malicious code in the parameters.
The sanitize_key() function is useful for protection against injections and XSS attacks, because:
- Removes potentially dangerous characters, such as <, >, ;, which may be part of JavaScript or HTML code.
- It is used for variables that can then be used in an HTML context: Clearing protects against code execution when transferring data between functions.
It is important to remember that sanitize_key() is intended for preparing strings as keys and is not suitable for sanitizing values that will be output to HTML, as it may only remove certain characters while leaving some other potentially dangerous values.
Protective functions:
WordPress has several built-in features to protect against XSS attacks, which help to safely process and output user data.
1.Esc_html()
This function is used to escape the data that will be output in the body of the HTML document. It prevents malicious scripts from executing by converting special characters into their HTML entities (for example, <becomes <lt;,> ->).
Example:
If user input is to be displayed on the page, for example, in the search field
if ( isset( $_GET['search'] ) ) {
$search_query = $_GET['search'];
echo "You searched for: " . esc_html( $search_query );
}
2. Esc_Attr()
It is used to escape data that will be inserted into HTML attributes (for example, the value of the href, src, value attribute, etc.). Escapes special characters such as quotes and angle brackets that can be used for JavaScript injections.
echo '<input type="text" value="' . esc_attr( $user_input ) . '".';
If the user enters data that should be inserted into the HTML attributes:
<input type="text" value="<?php echo esc_attr( $user_input ); ?>" />
3. ESC_JS()
The function escapes the data that will be inserted into JavaScript. It safely handles characters such as quotes, spaces, newlines, and others that may cause an error or be used for JavaScript injections.
Example:
echo '<script>console.log("Message: ' . esc_js( $user_input ) . '");</script>';
If the user’s data is passed to JavaScript, then you need to use the function:
<script> var userMessage = "<?php echo esc_js( $user_input ); ?>"; </script>
4. SANITIZE_TEXT_FIELD()
The main protection mechanism is to prevent the possibility of entering or displaying JavaScript code on web pages. When a user enters data into forms such as username, comments, or text fields, this value can be displayed on the screen without proper cleaning. If the data is not cleared, an attacker can insert malicious code that will be executed in another user’s browser (XSS attack). The sanitize_text_field() function helps eliminate such threats by removing tags and suspicious characters.
Using:
$clean_text = sanitize_text_field( $_GET['user_input'] );
Example:
if (isset($_GET['user_input'])) {
$clean_input = sanitize_text_field($_GET['user_input']);
echo "User input: " . $clean_input;
}
5. SANITIZE_KEY()
The function removes or escapes characters that can be used to perform attacks such as injections or other vulnerabilities. This includes:
Spaces.
Characters that are not within the acceptable range for keys, for example, characters used for injection or URL manipulation.
The function only allows characters that are safe to use in URLs, option names, hook names, or other keys. This is usually:
Letters (a-z, A-Z).
Numbers (0-9).
Underscores and dashes (for example, my_key, my-key).
if (isset($_GET['nav'])) {
$nav = sanitize_key($_GET['nav']);
echo "Selected nav: " . $nav;
}
6. SANITIZE_EMAIL()
The function removes all characters that are not part of the standard email address format, such as spaces, tabs, and other characters that are not allowed in email addresses according to the RFC standard. It is also important that the email address is structurally valid. sanitize_email() uses regular expressions to check and clear the string. For example, it checks that the email address contains the @ symbol and the domain, and removes all unnecessary characters, such as additional spaces or forbidden characters. Clearing email addresses helps prevent malicious scripts from being embedded in a database or on pages.
Example:
if (isset($_POST['submit'])) {
$user_email = $_POST['user_email'];
$clean_email = sanitize_email($user_email);
if (is_email($clean_email)) {
update_option('user_email', $clean_email);
echo "Your email has been successfully updated.";
} else {
echo "The email address you entered is not valid. Please try again.";
}
}
7. WP_KSES()
wp_kses() is a function in WordPress used to filter HTML content and prevent potentially harmful or unsafe HTML elements and attributes from being added to posts, comments, or other types of user input. This is one of the main functions designed to prevent cross-site scripting (XSS) attacks.
The main purpose of wp_kses() is to allow access only to a secure subset of HTML elements and attributes, while removing any potentially malicious code or elements that could lead to XSS vulnerabilities. When a user submits input data (for example, a comment, the content of a post, or a custom form) containing HTML, the wp_kses() function ensures that only the allowed HTML tags and attributes remain, and the rest will be deleted.
This is especially important if you want to store user-generated content in the database, such as blog posts or comments, while maintaining certain HTML formatting (such as links, images, or paragraphs) without the risk of running malicious scripts. By default, wp_kses() allows a set of safe HTML tags (like <a>, <b>, <p>, <img>, etc.) and certain attributes (like href, src, alt, etc.) for those tags. You can customize this list by providing a custom set of allowed tags and attributes. Any HTML tags, JavaScript code, event attributes (like onclick, onmouseover), or other potentially dangerous content (such as <script> or <style>) are removed. This ensures that any script injected by the user will not be executed when the content is viewed by others.
Example:
function wp_kses($content, $allowed_html) {
$allowed_tags = array_keys($allowed_html);
$content = strip_tags($content, '<' . implode('><', $allowed_tags) . '>');
return $content;
}
$content = '<p>This is <b>safe</b> content <script>alert("XSS")</script></p>';
$allowed_html = array(
"p" => array(),
"b" => array()
);
$clean_content = wp_kses($content, $allowed_html);
echo $clean_content;
The output will be: <p>This is <b>safe</b> content </p>
Conclusion
Following the basic recommendations outlined on this page, you will be able to minimize the risks of introducing the most common vulnerabilities into the code. The main focus should be on checking the input data, cleaning the output data and escaping, which will significantly strengthen the security of your application. When choosing cleaning and screening methods, it is important to focus on the most appropriate one for your particular case.
If you output data to HTML attributes, use special cleaning or escaping functions designed to work with HTML attributes. This will ensure a balance between performance and security. Proper protection against XSS vulnerabilities and correct processing of output data will help to avoid many potential threats in your application.