Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/custom-facebook-feed/admin/admin-functions.php
+++ b/custom-facebook-feed/admin/admin-functions.php
@@ -587,6 +587,10 @@
$admin_url_state = "http://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}";
}
+ // Mirrors CFF_oEmbeds::get_connection_url() — carries the same nonce so
+ // processCffOembedAccessToken() accepts this round-trip too.
+ $admin_url_state = add_query_arg( 'cff_oembed_nonce', wp_create_nonce( 'cff_oembed_connect' ), $admin_url_state );
+
return array(
'connect' => CFF_OEMBED_CONNECT_URL,
'cff_con' => $nonce,
--- a/custom-facebook-feed/admin/views/oembeds/content.php
+++ b/custom-facebook-feed/admin/views/oembeds/content.php
@@ -33,6 +33,7 @@
{{genericText.enable}}
</button>
</span>
+ <span class="cff-oembed-error" v-if="fboEmbedError">{{fboEmbedError}}</span>
</div>
<div class="cff-oembed-plugin-box cff-oembed-instagram">
<span class="oembed-icon" v-html="images.instaIcon"></span>
--- a/custom-facebook-feed/custom-facebook-feed.php
+++ b/custom-facebook-feed/custom-facebook-feed.php
@@ -1,9 +1,9 @@
<?php
/*
-Plugin Name: Smash Balloon Custom Facebook Feed
+Plugin Name: Smash Balloon Facebook Feed
Plugin URI: https://smashballoon.com/custom-facebook-feed
Description: Add completely customizable Facebook feeds to your WordPress site
-Version: 4.9.0
+Version: 4.10.0
Author: Smash Balloon
Author URI: http://smashballoon.com/
License: GPLv2 or later
@@ -27,7 +27,7 @@
if (! defined('ABSPATH')) {
exit; // Exit if accessed directly
}
-define( 'CFFVER', '4.9.0' );
+define( 'CFFVER', '4.10.0' );
if ( ! defined( 'CFF_SMASH_USAGE_TRACKING_API_URL' ) ) {
define( 'CFF_SMASH_USAGE_TRACKING_API_URL', 'https://usage.smashballoon.com/api' );
}
--- a/custom-facebook-feed/inc/Admin/CFF_oEmbeds.php
+++ b/custom-facebook-feed/inc/Admin/CFF_oEmbeds.php
@@ -50,6 +50,7 @@
add_action('admin_menu', [$this, 'register_menu']);
+ add_action( 'wp_ajax_cff_oembed_connect_init', array( $this, 'oembed_connect_init' ) );
add_action('wp_ajax_disable_facebook_oembed', [$this, 'disable_facebook_oembed']);
add_action('wp_ajax_disable_instagram_oembed', [$this, 'disable_instagram_oembed']);
}
@@ -77,6 +78,42 @@
}
/**
+ * Marks an oEmbed connect as started for the current user.
+ *
+ * The external connect service redirects back with cff_access_token on a plain page
+ * load and does not carry any of our query parameters through, so there is no nonce on
+ * the inbound request to verify. This one-shot, short-lived, per-user marker stands in
+ * for the OAuth "state" value: only a real click on Connect (an authenticated,
+ * nonce-checked, same-origin POST) can set it, so a cross-site GET carrying an
+ * attacker's token has nothing to consume. processCffOembedAccessToken() deletes it on use.
+ *
+ * @since 4.9.1
+ *
+ * @return void
+ */
+ public function oembed_connect_init() {
+ // Same gate as the sibling disable_*_oembed handlers: wp_die()s on either a bad
+ // nonce or an insufficient capability, rather than returning a 200 with an error
+ // payload. The caller treats any non-success response as a failed init.
+ CustomFacebookFeedBuilderCFF_Feed_Builder::check_privilege( 'nonce' );
+
+ set_transient( self::connect_pending_key(), 1, 15 * MINUTE_IN_SECONDS );
+
+ wp_send_json_success();
+ }
+
+ /**
+ * Transient key holding the "connect started" marker for the current user.
+ *
+ * @since 4.9.1
+ *
+ * @return string
+ */
+ private static function connect_pending_key() {
+ return 'cff_oembed_connect_pending_' . get_current_user_id();
+ }
+
+ /**
* Disable Facebook oEmbed
*
* @since 4.0
@@ -191,6 +228,7 @@
'instagramOEmbeds' => __('Instagram oEmbeds are currently not being handled by Smash Balloon', 'custom-facebook-feed'),
'instagramOEmbedsEnabled' => __('Instagram oEmbeds are turned on', 'custom-facebook-feed'),
'enable' => __('Enable', 'custom-facebook-feed'),
+ 'connectError' => __( 'Could not start the connection. Please reload this page and try again.', 'custom-facebook-feed' ),
'disable' => __('Disable', 'custom-facebook-feed'),
'whatAreOembeds' => __('What are oEmbeds?', 'custom-facebook-feed'),
'whatElseOembeds' => __('What else can the Custom Facebook Feed plugin do?', 'custom-facebook-feed'),
@@ -278,6 +316,11 @@
if ($admin_url_state === '/wp-admin/admin.php?page=cff-oembeds-manager') {
$admin_url_state = "http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
}
+ // Carry a dedicated nonce on the return URL where the connect service preserves the
+ // state query string. processCffOembedAccessToken() verifies this nonce — or the
+ // cff_con nonce POSTed below — before accepting cff_access_token, and falls back to
+ // the one-shot connect marker when the service returns neither.
+ $admin_url_state = add_query_arg( 'cff_oembed_nonce', wp_create_nonce( 'cff_oembed_connect' ), $admin_url_state );
return array(
'connect' => CFF_OEMBED_CONNECT_URL,
'cff_con' => $nonce,
@@ -333,6 +376,27 @@
{
global $cff_notices;
$return = [];
+
+ // Never accept cff_access_token from an unsolicited request. The connect service
+ // returns none of our parameters, so authorise on the one-shot marker that
+ // oembed_connect_init() sets when this user actually clicks Connect. A nonce is
+ // still honoured first for any return URL that does preserve the query string.
+ $oembed_nonce = isset( $_GET['cff_oembed_nonce'] ) ? sanitize_key( wp_unslash( $_GET['cff_oembed_nonce'] ) ) : '';
+ $connect_nonce = isset( $_GET['cff_con'] ) ? sanitize_key( wp_unslash( $_GET['cff_con'] ) ) : '';
+
+ $has_valid_nonce = wp_verify_nonce( $oembed_nonce, 'cff_oembed_connect' )
+ || wp_verify_nonce( $connect_nonce, 'cff_con' );
+
+ if ( ! $has_valid_nonce ) {
+ $pending_key = self::connect_pending_key();
+ if ( ! get_transient( $pending_key ) ) {
+ $return['error'] = 'Invalid Nonce';
+ return $return;
+ }
+ // One shot: a replay of the same URL is rejected.
+ delete_transient( $pending_key );
+ }
+
$access_token = $_GET['cff_access_token'];
$valid_new_access_token = !empty($access_token) && strlen($access_token) > 20 && $saved_access_token_data !== $access_token ?
--- a/custom-facebook-feed/inc/CFF_FB_Settings.php
+++ b/custom-facebook-feed/inc/CFF_FB_Settings.php
@@ -488,6 +488,11 @@
$legacy_settings_with_updated_defaults = wp_parse_args($options, CustomFacebookFeedBuilderCFF_Feed_Saver::settings_defaults());
+ // Constrain the merge to recognized setting keys only — arbitrary or
+ // unexpected attribute names must never be merged into the settings that feed
+ // markup-shaping sinks.
+ $shortcode_atts = array_intersect_key( (array) $shortcode_atts, $legacy_settings_with_updated_defaults );
+
$legacy_settings = wp_parse_args($shortcode_atts, $legacy_settings_with_updated_defaults);
$legacy_settings['id'] = ! empty($legacy_settings['account']) ? $legacy_settings['account'] : $legacy_settings['id'];
--- a/custom-facebook-feed/inc/CFF_Shortcode.php
+++ b/custom-facebook-feed/inc/CFF_Shortcode.php
@@ -441,7 +441,10 @@
}
// See Less text
- $cff_posttext_link_color = str_replace('#', '', $this->atts['textlinkcolor']);
+ // Constrain to a hex colour at the source — this value feeds multiple
+ // style="..." attributes below.
+ $cff_posttext_link_color_raw = ltrim( (string) $this->atts['textlinkcolor'], '#' );
+ $cff_posttext_link_color = preg_match( '/^[0-9a-fA-F]{3,8}$/', $cff_posttext_link_color_raw ) ? $cff_posttext_link_color_raw : '';
$cff_title_link = CFF_Utils::check_if_on($this->atts['textlink']);
// Description Style
@@ -1146,7 +1149,7 @@
$cff_content .= CFF_Utils::print_template_part('credit', get_defined_vars());
// End the feed
- $cff_content .= '<input class="cff-pag-url" type="hidden" data-locatornonce="' . esc_attr(wp_create_nonce('cff-locator-nonce-' . get_the_ID())) . '" data-cff-shortcode="' . $data_att_html . '" data-post-id="' . get_the_ID() . '" data-feed-id="' . $atts['id'] . '">';
+ $cff_content .= '<input class="cff-pag-url" type="hidden" data-locatornonce="' . esc_attr( wp_create_nonce( 'cff-locator-nonce-' . get_the_ID() ) ) . '" data-cff-shortcode="' . $data_att_html . '" data-post-id="' . get_the_ID() . '" data-feed-id="' . esc_attr( $atts['id'] ) . '">';
$cff_content .= '</div></div><div class="cff-clear"></div>';
// Add the Like Box outside
--- a/custom-facebook-feed/inc/CFF_Shortcode_Display.php
+++ b/custom-facebook-feed/inc/CFF_Shortcode_Display.php
@@ -384,7 +384,10 @@
*/
public static function get_author_name($news)
{
- return isset($news->from->name) ? str_replace('"', "", $news->from->name) : '';
+ // Entity-encode the Facebook-sourced author display name — it is echoed into
+ // element content with no further escaping at the template sink, so this
+ // getter is the safe boundary.
+ return isset( $news->from->name ) ? str_replace( '"', '', htmlentities( $news->from->name, ENT_QUOTES, 'UTF-8' ) ) : '';
}
public static function get_author_link_atts($news, $target, $cff_nofollow, $cff_author_styles)
@@ -622,7 +625,7 @@
// Add the button to the post if the text isn't "NO_BUTTON"
if ($cff_button_type != 'NO_BUTTON') :
?>
- <p class="cff-cta-link" <?php echo $cff_title_styles ?>><a href="<?php echo esc_url($cff_cta_link) ?>" target="_blank" data-app-link="<?php echo $cff_app_link ?>" style="color: #<?php echo $cff_posttext_link_color ?>;" <?php echo $cff_nofollow_referrer ?> ><?php echo $cff_cta_button_text ?></a></p>
+ <p class="cff-cta-link" <?php echo $cff_title_styles; ?>><a href="<?php echo esc_url( $cff_cta_link ); ?>" target="_blank" data-app-link="<?php echo esc_url( $cff_app_link ); ?>" style="color: #<?php echo esc_attr( $cff_posttext_link_color ); ?>;" <?php echo $cff_nofollow_referrer; ?> ><?php echo esc_html( $cff_cta_button_text ); ?></a></p><?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $cff_title_styles and $cff_nofollow_referrer are internally-built attribute strings (style="..." / rel="..."), not data; escaping them would emit the markup literally. ?>
<?php
endif;
}
@@ -652,12 +655,16 @@
public static function get_shared_link_title_format($atts)
{
- return ( empty($atts[ 'linktitleformat' ]) ) ? 'p' : $atts[ 'linktitleformat' ];
+ // Allow-list the tag name; fall back to a safe default.
+ $cff_link_title_tag = empty( $atts['linktitleformat'] ) ? 'p' : sanitize_key( $atts['linktitleformat'] );
+ $cff_allowed_link_title_tags = array( 'p', 'span', 'div', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a' );
+ return in_array( $cff_link_title_tag, $cff_allowed_link_title_tags, true ) ? $cff_link_title_tag : 'p';
}
public static function get_shared_link_title_styles($atts)
{
- return ( !empty($atts[ 'linktitlesize' ]) && $atts[ 'linktitlesize' ] != 'inherit' ) ? 'style="font-size:' . $atts[ 'linktitlesize' ] . 'px;"' : '';
+ // Cast to (int) before concatenating into the style="..." attribute below.
+ return ( ! empty( $atts['linktitlesize'] ) && 'inherit' !== $atts['linktitlesize'] ) ? 'style="font-size:' . (int) $atts['linktitlesize'] . 'px;"' : '';
}
--- a/custom-facebook-feed/templates/item/post-text.php
+++ b/custom-facebook-feed/templates/item/post-text.php
@@ -36,7 +36,7 @@
</a>
<?php endif; ?>
</span>
- <span class="cff-expand">... <a href="#" style="color: #<?php echo $cff_posttext_link_color; ?>"><span class="cff-more"><?php echo esc_html($atts[ 'seemoretext' ]); ?></span><span class="cff-less"><?php echo esc_html($atts[ 'seelesstext' ]); ?></span></a></span>
+ <span class="cff-expand">... <a href="#" style="color: #<?php echo esc_attr( $cff_posttext_link_color ); ?>"><span class="cff-more"><?php echo esc_html( $atts['seemoretext'] ); ?></span><span class="cff-less"><?php echo esc_html( $atts['seelesstext'] ); ?></span></a></span>
</<?php echo $cff_title_format ?>>
--- a/custom-facebook-feed/templates/item/shared-link.php
+++ b/custom-facebook-feed/templates/item/shared-link.php
@@ -21,7 +21,10 @@
$cff_link_caption = CFF_Shortcode_Display::get_shared_link_caption($news);
$cff_link_title_format = CFF_Shortcode_Display::get_shared_link_title_format($atts);
$cff_link_title_styles = CFF_Shortcode_Display::get_shared_link_title_styles($atts);
-$cff_link_title_color = str_replace('#', '', $atts[ 'linktitlecolor' ]);
+// Constrain to a hex colour at the source (mirrors the textlinkcolor guard in
+// CFF_Shortcode.php) — this value feeds a style="..." attribute below.
+$cff_link_title_color_raw = ltrim( (string) $atts['linktitlecolor'], '#' );
+$cff_link_title_color = preg_match( '/^[0-9a-fA-F]{3,8}$/', $cff_link_title_color_raw ) ? $cff_link_title_color_raw : '';
if ($cff_post_type == 'link' || $cff_soundcloud || $cff_is_video_embed) :
@@ -31,7 +34,7 @@
<div class="cff-text-link cff-no-image">
<?php if (isset($news->name)) : ?>
<<?php echo $cff_link_title_format ?> class="cff-link-title" <?php echo $cff_link_title_styles; ?>>
- <a href="<?php echo esc_url($link) ?>" <?php echo $target . ' ' . $cff_nofollow_referrer; ?> style="color:#<?php echo $cff_link_title_color; ?>;"><?php echo $news->name; ?></a>
+ <a href="<?php echo esc_url( $link ); ?>" <?php echo $target . ' ' . $cff_nofollow_referrer; ?> style="color:#<?php echo esc_attr( $cff_link_title_color ); ?>;"><?php echo esc_html( $news->name ); ?></a><?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $target and $cff_nofollow_referrer are internally-built literal attribute strings (target="..." / rel="..."), not data. ?>
</<?php echo $cff_link_title_format ?>>
<?php endif; ?>
--- a/custom-facebook-feed/vendor/composer/installed.php
+++ b/custom-facebook-feed/vendor/composer/installed.php
@@ -2,4 +2,4 @@
namespace FacebookFeedVendor;
-return array('root' => array('name' => 'smashballoon/custom-facebook-feed-pro', 'pretty_version' => 'v4.9.0', 'version' => '4.9.0.0', 'reference' => '2e8248f162c218378d12772e9846db2ded0c0196', '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-facebook-feed-pro' => array('pretty_version' => 'v4.9.0', 'version' => '4.9.0.0', 'reference' => '2e8248f162c218378d12772e9846db2ded0c0196', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false), 'smashballoon/framework' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '3c53a9a74cc5bbdb4df2cfec13007f4793404e0f', 'type' => 'library', 'install_path' => __DIR__ . '/../smashballoon/framework', 'aliases' => array(0 => '9999999-dev'), 'dev_requirement' => false)));
+return array('root' => array('name' => 'smashballoon/custom-facebook-feed-pro', 'pretty_version' => 'v4.10.0', 'version' => '4.10.0.0', 'reference' => 'e2a8bba261f0837274681207b3864b3fecde7d05', '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-facebook-feed-pro' => array('pretty_version' => 'v4.10.0', 'version' => '4.10.0.0', 'reference' => 'e2a8bba261f0837274681207b3864b3fecde7d05', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false), 'smashballoon/framework' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '3c53a9a74cc5bbdb4df2cfec13007f4793404e0f', 'type' => 'library', 'install_path' => __DIR__ . '/../smashballoon/framework', 'aliases' => array(0 => '9999999-dev'), 'dev_requirement' => false)));