Blackhole for Bad Bots <= 3.8 - Unauthenticated Stored Cross-Site Scripting via User-Agent HTTP Header
Description
The Blackhole for Bad Bots plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the User-Agent HTTP header in all versions up to and including 3.8. This is due to insufficient input sanitization and output escaping. The plugin uses sanitize_text_field() when capturing bot data (which strips HTML tags but does not escape HTML entities like double quotes), then stores the data via update_option(). When an administrator views the Bad Bots log page, the stored data is output directly into HTML input value attributes (lines 75-83) without esc_attr() and into HTML span content without esc_html(). This makes it possible for unauthenticated attackers to inject arbitrary web scripts that execute when an administrator views the Blackhole Bad Bots admin page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=3.8What Changed in the Fix
Changes introduced in v3.8.1
Source Code
WordPress.org SVN# Research Plan: Blackhole for Bad Bots Stored XSS ## 1. Vulnerability Summary The **Blackhole for Bad Bots** plugin (<= 3.8) is vulnerable to **Unauthenticated Stored Cross-Site Scripting (XSS)** via the `User-Agent` HTTP header. The plugin is designed to trap "bad bots" that ignore `robots.txt`…
Show full research plan
Research Plan: Blackhole for Bad Bots Stored XSS
1. Vulnerability Summary
The Blackhole for Bad Bots plugin (<= 3.8) is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS) via the User-Agent HTTP header.
The plugin is designed to trap "bad bots" that ignore robots.txt rules and visit a hidden "blackhole" link. When a bot hits this link, the plugin captures its request metadata (IP, User-Agent, etc.) and stores it in the WordPress database (within the bbb_badbots option).
The vulnerability exists because:
- Insufficient Sanitization: Captured
User-Agentdata is processed usingsanitize_text_field(). While this function strips HTML tags, it does not escape quotes or HTML entities. - Lack of Output Escaping: When an administrator views the "Bad Bots" log in the WordPress dashboard (
wp-admin/admin.php?page=blackhole_badbots), the storedUser-Agentis output directly into HTMLvalueattributes ofinputtags (used for form persistence) and into<span>content without usingesc_attr()oresc_html().
This allows an unauthenticated attacker to inject attributes into the input tags (e.g., onfocus) to execute arbitrary JavaScript when the administrator views the log.
2. Attack Vector Analysis
- Endpoint: Any public-facing page (to find the trap) and the "blackhole" trap URL itself.
- Trap URL: Typically
/?blackholeor a randomized path added torobots.txt. - HTTP Parameter:
User-AgentHTTP header. - Authentication: Unauthenticated (the bot is by definition an external visitor).
- Preconditions:
- The plugin must be active.
- The attacker must identify the forbidden "blackhole" link path.
3. Code Flow
- Entry Point:
blackhole_scanner()(registered inblackhole.phpline 98) runs on theinithook for every request. - Detection:
blackhole_scannerchecks if the current request URI matches the "blackhole" trigger link. - Data Capture: If matched, the plugin gathers
$_SERVER['HTTP_USER_AGENT']. - Storage: The plugin updates the
bbb_badbotsoption. - Sanitization Sink: The
register_settingforbbb_badbots(defined ininc/badbots-register.phpline 9) specifiesblackhole_validate_badbotsas the validation callback. - Validation:
blackhole_validate_badbots(line 21) runssanitize_text_field()on theuser_agentfield.- Input:
EvilBot " onfocus="alert(1)" autofocus=" - Output:
EvilBot " onfocus="alert(1)" autofocus="(Quotes are preserved).
- Input:
- Output Sink: An administrator navigates to the Bad Bots page. The function
blackhole_callback_blocked_bots()(line 64) is called.- Line 85:
echo '<input name="bbb_badbots['. $key .'][user_agent]" type="hidden" value="'. $user_agent .'" /> '; - The injected quote breaks out of the
valueattribute, andautofocus+onfocusexecutes the script.
- Line 85:
4. Nonce Acquisition Strategy
No nonce is required.
The vulnerability is triggered by an unauthenticated GET request to the "blackhole" trap URL. The blackhole_scanner() logic on init does not perform nonce verification because its purpose is to catch automated bots which would not possess a WordPress nonce.
5. Exploitation Strategy
- Identify the Trap:
- Navigate to the site's homepage using
browser_navigate. - Check the HTML source for a hidden link (usually in the footer, as per
blackhole.phplines 96-97). - Alternatively, check
/robots.txtfor aDisallowrule pointing to a blackhole path.
- Navigate to the site's homepage using
- Submit Payload:
- Send an HTTP GET request to the identified trap URL using
http_request. - Set the
User-Agentheader to a payload that breaks out of an HTML attribute.
- Send an HTTP GET request to the identified trap URL using
- Payload Design:
- Since
sanitize_text_fieldis used, we cannot use<script>tags. - We must use attribute breakout:
EvilBot " onfocus="alert(1)" autofocus=" - Note: Avoid the strings "googlebot", "bingbot", "chrome", or "wordpress" as they are whitelisted (see
blackhole.phpline 182).
- Since
- Trigger Execution:
- Log in to the WordPress admin panel.
- Navigate to
http://localhost:8080/wp-admin/admin.php?page=blackhole_badbots. - The
inputtag will render as:<input ... value="EvilBot " onfocus="alert(1)" autofocus="" />. - The
autofocusattribute will triggeronfocusimmediately upon page load in modern browsers.
6. Test Data Setup
- Plugin Installation: Ensure "Blackhole for Bad Bots" version 3.8 is installed and activated.
- Configuration: The plugin defaults are usually sufficient.
- Admin User: A standard administrator account to view the logs.
- Shortcode/Page: No specific shortcode is needed to trigger the vulnerability, as the trap is typically appended to the footer of all pages by
blackhole_triggeronwp_footer.
7. Expected Results
- The malicious request is logged by the plugin.
- The
bbb_badbotsoption in thewp_optionstable contains the payload string with quotes intact. - Upon visiting the Bad Bots admin page, a JavaScript
alert(1)(or equivalent) executes in the context of the administrator session.
8. Verification Steps
- Check Database via WP-CLI:
Verify that thewp option get bbb_badbots --format=jsonuser_agentfield for one of the entries contains the raw payload string. - Check HTML Output via HTTP Request (Admin Session):
- Perform an authenticated request to
/wp-admin/admin.php?page=blackhole_badbots. - Inspect the response body for the unescaped payload:
<input name="bbb_badbots[1][user_agent]" type="hidden" value="EvilBot " onfocus="alert(1)" autofocus="" /> - Perform an authenticated request to
9. Alternative Approaches
If autofocus is filtered or blocked by the browser, use a payload that creates a larger invisible overlay to trigger on mouse movement:
- Payload:
EvilBot " style="position:fixed;top:0;left:0;width:100%;height:100%;display:block;" onmouseover="alert(1) - If
sanitize_text_fieldinterferes with thestyleattribute, tryonmouseenteroronclickon the hidden input field, thoughonfocuswithautofocusis the most reliable for hidden inputs. - Note that even though the input is
type="hidden", most browsers will still honorautofocusandonfocusif they are manually injected into the DOM as attributes. Iftype="hidden"prevents focus, we can attempt to injecttype="text"to overwrite the earliertypeattribute (if the browser allows attribute precedence for the first occurrence).
Summary
The Blackhole for Bad Bots plugin for WordPress is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS) due to insufficient sanitization and output escaping of the User-Agent HTTP header. Attackers can trigger the vulnerability by visiting the site's hidden "blackhole" trap URL with a crafted User-Agent string, which then executes malicious JavaScript in the browser of an administrator viewing the Bad Bots log page.
Vulnerable Code
// inc/badbots-register.php:21 function blackhole_validate_badbots($bots) { foreach ($bots as $key => $val) { $bots[$key]['ip_address'] = isset($bots[$key]['ip_address']) ? sanitize_text_field($bots[$key]['ip_address']) : null; $bots[$key]['request_uri'] = isset($bots[$key]['request_uri']) ? sanitize_text_field($bots[$key]['request_uri']) : null; $bots[$key]['remote_host'] = isset($bots[$key]['remote_host']) ? sanitize_text_field($bots[$key]['remote_host']) : null; $bots[$key]['query_string'] = isset($bots[$key]['query_string']) ? sanitize_text_field($bots[$key]['query_string']) : null; $bots[$key]['user_agent'] = isset($bots[$key]['user_agent']) ? sanitize_text_field($bots[$key]['user_agent']) : null; $bots[$key]['referrer'] = isset($bots[$key]['referrer']) ? sanitize_text_field($bots[$key]['referrer']) : null; $bots[$key]['protocol'] = isset($bots[$key]['protocol']) ? sanitize_text_field($bots[$key]['protocol']) : null; $bots[$key]['method'] = isset($bots[$key]['method']) ? sanitize_text_field($bots[$key]['method']) : null; $bots[$key]['date'] = isset($bots[$key]['date']) ? sanitize_text_field($bots[$key]['date']) : null; } return $bots; } --- // inc/badbots-register.php:75 echo '<input name="bbb_badbots['. $key .'][ip_address]" type="hidden" value="'. $ip_address .'" /> '; echo '<input name="bbb_badbots['. $key .'][request_uri]" type="hidden" value="'. $request_uri .'" /> '; echo '<input name="bbb_badbots['. $key .'][remote_host]" type="hidden" value="'. $remote_host .'" /> '; echo '<input name="bbb_badbots['. $key .'][query_string]" type="hidden" value="'. $query_string .'" /> '; echo '<input name="bbb_badbots['. $key .'][user_agent]" type="hidden" value="'. $user_agent .'" /> '; echo '<input name="bbb_badbots['. $key .'][referrer]" type="hidden" value="'. $referrer .'" /> '; echo '<input name="bbb_badbots['. $key .'][protocol]" type="hidden" value="'. $protocol .'" /> '; echo '<input name="bbb_badbots['. $key .'][method]" type="hidden" value="'. $method .'" /> '; echo '<input name="bbb_badbots['. $key .'][date]" type="hidden" value="'. $date .'" /> '; $data = '<strong>'. $date .'</strong> - '. $ip_address .' - '. $protocol .' - <span class="bbb-user-agent">'. $user_agent . '</span>';
Security Fix
@@ -114,7 +114,7 @@ private function constants() { if (!defined('BBB_REQUIRE')) define('BBB_REQUIRE', '4.7'); - if (!defined('BBB_VERSION')) define('BBB_VERSION', '3.8'); + if (!defined('BBB_VERSION')) define('BBB_VERSION', '3.8.1'); if (!defined('BBB_NAME')) define('BBB_NAME', 'Blackhole for Bad Bots'); if (!defined('BBB_AUTHOR')) define('BBB_AUTHOR', 'Jeff Starr'); if (!defined('BBB_HOME')) define('BBB_HOME', 'https://perishablepress.com/blackhole-bad-bots/'); @@ -72,17 +72,17 @@ $method = isset($val['method']) ? $val['method'] : ''; $date = isset($val['date']) ? $val['date'] : ''; - echo '<input name="bbb_badbots['. $key .'][ip_address]" type="hidden" value="'. $ip_address .'" /> '; - echo '<input name="bbb_badbots['. $key .'][request_uri]" type="hidden" value="'. $request_uri .'" /> '; - echo '<input name="bbb_badbots['. $key .'][remote_host]" type="hidden" value="'. $remote_host .'" /> '; - echo '<input name="bbb_badbots['. $key .'][query_string]" type="hidden" value="'. $query_string .'" /> '; - echo '<input name="bbb_badbots['. $key .'][user_agent]" type="hidden" value="'. $user_agent .'" /> '; - echo '<input name="bbb_badbots['. $key .'][referrer]" type="hidden" value="'. $referrer .'" /> '; - echo '<input name="bbb_badbots['. $key .'][protocol]" type="hidden" value="'. $protocol .'" /> '; - echo '<input name="bbb_badbots['. $key .'][method]" type="hidden" value="'. $method .'" /> '; - echo '<input name="bbb_badbots['. $key .'][date]" type="hidden" value="'. $date .'" /> '; + echo '<input name="bbb_badbots['. $key .'][ip_address]" type="hidden" value="'. esc_attr($ip_address) .'" /> '; + echo '<input name="bbb_badbots['. $key .'][request_uri]" type="hidden" value="'. esc_attr($request_uri) .'" /> '; + echo '<input name="bbb_badbots['. $key .'][remote_host]" type="hidden" value="'. esc_attr($remote_host) .'" /> '; + echo '<input name="bbb_badbots['. $key .'][query_string]" type="hidden" value="'. esc_attr($query_string) .'" /> '; + echo '<input name="bbb_badbots['. $key .'][user_agent]" type="hidden" value="'. esc_attr($user_agent) .'" /> '; + echo '<input name="bbb_badbots['. $key .'][referrer]" type="hidden" value="'. esc_attr($referrer) .'" /> '; + echo '<input name="bbb_badbots['. $key .'][protocol]" type="hidden" value="'. esc_attr($protocol) .'" /> '; + echo '<input name="bbb_badbots['. $key .'][method]" type="hidden" value="'. esc_attr($method) .'" /> '; + echo '<input name="bbb_badbots['. $key .'][date]" type="hidden" value="'. esc_attr($date) .'" /> '; - $data = '<strong>'. $date .'</strong> - '. $ip_address .' - '. $protocol .' - <span class="bbb-user-agent">'. $user_agent . '</span>'; + $data = '<strong>'. esc_html($date) .'</strong> - '. esc_html($ip_address) .' - '. esc_html($protocol) .' - <span class="bbb-user-agent">'. esc_html($user_agent) . '</span>';
Exploit Outline
1. Identify the hidden "blackhole" trap URL on the target WordPress site (found either in the robots.txt file or as a hidden link in the page source). 2. Send an unauthenticated GET request to the trap URL. 3. Include a malicious User-Agent header in the request (e.g., `" onfocus="alert(1)" autofocus="`). 4. The plugin captures this header, passes it through `sanitize_text_field` (which preserves quotes), and saves it to the `bbb_badbots` WordPress option. 5. Wait for an administrator to view the "Bad Bots" log page in the WordPress admin dashboard (`/wp-admin/admin.php?page=blackhole_badbots`). 6. The payload breaks out of the HTML `value` attribute of a hidden input tag, injecting the `onfocus` and `autofocus` attributes, which execute the JavaScript immediately upon page load.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.