CVE-2026-6177

Custom Twitter Feeds <= 2.5.4 - Unauthenticated Stored Cross-Site Scripting via Cached Tweet Text

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
2.5.5
Patched in
2d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2.5.4
PublishedMay 12, 2026
Last updatedMay 13, 2026
Affected plugincustom-twitter-feeds

What Changed in the Fix

Changes introduced in v2.5.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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 both wp_ajax_ and wp_ajax_nopriv_)
  • Vulnerable Parameter: The payload is retrieved from the database (Stored XSS). The trigger is the AJAX request.
  • Authentication: Unauthenticated.
  • Preconditions:
    1. A Twitter feed must be configured or a dummy feed created.
    2. Malicious HTML must be present in the twitter_text (or equivalent) column of the ctf_posts table.
    3. A valid AJAX nonce must be provided (if enforced).

3. Code Flow

  1. Entry Point: An unauthenticated user sends a POST request to admin-ajax.php with action=ctf_get_more_posts.
  2. AJAX Handler: The handler (likely in inc/ctf-functions.php) identifies the feed and queries the ctf_posts table (defined by the constant CTF_POSTS_TABLE) for the next batch of tweets.
  3. Data Retrieval: The tweet text is fetched from the database.
  4. Sink Call: The plugin iterates through the tweets and calls CTF_Display_Elements::get_post_text($post_data, $feed_options).
  5. Rendering: get_post_text() processes the tweet text using nl2br() to preserve line breaks but fails to call esc_html().
  6. 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.

  1. Shortcode: The primary shortcode is [custom-twitter-feeds].
  2. Setup: Create a public page containing this shortcode.
  3. Navigation: Use the browser_navigate tool to visit this page.
  4. Extraction: The nonce and AJAX URL are typically localized in the ctf_script global 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 nonce within the ctf_script object).

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_posts
    • feed_id: 1 (or the ID of the feed created in setup)
    • ctf_nonce: [EXTRACTED_NONCE]
    • num: 5
    • offset: 0

6. Test Data Setup

  1. Activate Plugin: Ensure custom-twitter-feeds is active.
  2. 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.
    
  3. Publish a Page:
    wp post create --post_type=page --post_title="Twitter Feed" --post_status=publish --post_content='[custom-twitter-feeds]'
    
  4. Populate Table: Manually insert the XSS payload into the ctf_posts table as described in the Exploitation Strategy.

7. Expected Results

  • The AJAX response will return a JSON object.
  • Within the html or output key of that JSON, the string <img src=x onerror=alert("XSS_PROVEN")> (or its nl2br version) will be present literally.
  • Specifically, it will not be converted to &lt;img ... &gt;.
  • When rendered in a browser context, the onerror event will fire.

8. Verification Steps

  1. HTTP Verification: Inspect the body of the http_request response.
    • Search for the raw string: onerror=alert
  2. DOM Verification: Use browser_navigate to 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.

9. Alternative Approaches

  • Feed Type Manipulation: If ctf_get_more_posts behaves differently for different feed types (e.g., hashtag vs usertimeline), try setting the type parameter in the AJAX request.
  • Cache Table Variants: Depending on the specific version, the table might be wp_ctf_posts or data might be stored in wp_options as a transient. If the ctf_posts table 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.
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/custom-twitter-feeds/2.5.4/inc/CTF_Display_Elements.php /home/deploy/wp-safety.org/data/plugin-versions/custom-twitter-feeds/2.5.5/inc/CTF_Display_Elements.php
--- /home/deploy/wp-safety.org/data/plugin-versions/custom-twitter-feeds/2.5.4/inc/CTF_Display_Elements.php	2026-03-12 18:47:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/custom-twitter-feeds/2.5.5/inc/CTF_Display_Elements.php	2026-04-30 13:44:54.000000000 +0000
@@ -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.