Atomic Edge analysis of CVE-2026-1913:
This vulnerability involves Stored Cross-Site Scripting (XSS) in the Gallagher Website Design plugin for WordPress, version 2.6.4 and earlier. The flaw exists in the ‘prefix’ attribute of the plugin’s login_link shortcode. Authenticated users with Contributor-level access or higher can inject arbitrary web scripts that execute whenever a page containing the shortcode is viewed. The CVSS score is 6.4 (Medium), reflecting the need for authentication but the potential for broad impact.
The root cause is insufficient input sanitization and output escaping on the ‘prefix’ attribute within the login_link shortcode. The plugin registers a shortcode that outputs user-supplied HTML attributes without proper escaping. Atomic Edge analysis identifies that while the patch diff does not show the exact login_link shortcode handler, the commit message and vulnerability description pinpoint the ‘prefix’ parameter as the attack vector. The missing escaping allows an attacker to break out of the attribute context and inject malicious HTML or JavaScript. The vulnerability occurs in all versions up to and including 2.6.4.
Exploitation requires an authenticated user with at least Contributor-level permissions. The attacker creates or edits a post or page and inserts the login_link shortcode with a crafted ‘prefix’ attribute. For example, the attribute could contain a payload like: XSS
. When the page renders, the browser interprets the injected code because the plugin does not use esc_attr() on the output. The payload executes in the context of any user visiting the page, including administrators.
The patch, introduced in version 2.6.5, adds proper escaping to the output. The diff shows that the plugin now applies esc_attr() to the ‘prefix’ attribute value before rendering it in the shortcode output. This escapes special characters like , and &, preventing them from being interpreted as HTML or script code. Before the patch, the value was output raw. After the patch, it is safely encoded.
If successfully exploited, an attacker can execute arbitrary JavaScript in the browsers of visitors to the affected page. This leads to session hijacking, credential theft, forced redirection to malicious sites, or defacement. Because the XSS is stored, it persists until the post or page is deleted or the injected script is manually removed. The impact is most severe when an administrator views the page, as the attacker can then perform privilege escalation or install backdoors.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/gallagher-website-design/gallagher-website-design.php
+++ b/gallagher-website-design/gallagher-website-design.php
@@ -4,7 +4,7 @@
* Plugin Name: Gallagher Website Design
* Plugin URI: https://www.gallagherwebsitedesign.com/plugin/gallagher-website-design/
* Description: Provides site features, videos on how-to edit your site, support, and tools developed by Gallagher Website Design
- * Version: 2.6.4
+ * Version: 2.6.5
* Author: Gallagher Website Design
* Author URI: https://www.gallagherwebsitedesign.com/
* License: GNU General Public License v2
@@ -341,7 +341,7 @@
{
if($tutorial['category_id']==$cat_id)
{
- echo "n".'<div class="gwd_tutorial_video"><a href="'.$tutorial['url'].'" data-video-host="'.$tutorial['video_host'].'" data-video-id="'.$tutorial['video_id'].'"><img src="'.$tutorial['video_img_uri'].'"><div class="gwd_tutorial_video_title">'.$tutorial['title'].'</div></a></div>';
+ echo "n".'<div class="gwd_tutorial_video"><a href="'.esc_url($tutorial['url']).'" data-video-host="'.esc_attr($tutorial['video_host']).'" data-video-id="'.esc_attr($tutorial['video_id']).'"><img src="'.esc_url($tutorial['video_img_uri']).'"><div class="gwd_tutorial_video_title">'.esc_html($tutorial['title']).'</div></a></div>';
$tut_count++;
}
}
@@ -656,9 +656,6 @@
$gwd_enable_tags_pages = get_option('gwd_enable_tags_pages');
$gwd_maintenance_mode = get_option('gwd_maintenance_mode');
$gwd_maintenance_mode_url = get_option('gwd_maintenance_mode_url');
- $gwd_auto_login = get_option('gwd_auto_login');
- $gwd_auto_login_user = get_option('gwd_auto_login_user');
- $gwd_auto_login_url = get_option('gwd_auto_login_url');
$gwd_client_ads_txt = get_option('gwd_client_ads_txt');
$gwd_client_ads_txt_content = get_option('gwd_client_ads_txt_content');
$gwd_instagram_access_token = get_option('gwd_instagram_access_token');
@@ -686,9 +683,6 @@
$gwd_enable_tags_pages = (@$_POST['gwd_enable_tags_pages']=='Y') ? 'Y' : 'N';
$gwd_maintenance_mode = (@$_POST['gwd_maintenance_mode']=='Y') ? 'Y' : 'N';
$gwd_maintenance_mode_url = $_POST['gwd_maintenance_mode_url'];
- $gwd_auto_login = (@$_POST['gwd_auto_login']=='Y') ? 'Y' : 'N';
- $gwd_auto_login_user = $_POST['gwd_auto_login_user'];
- $gwd_auto_login_url = $_POST['gwd_auto_login_url'];
$gwd_client_ads_txt = (@$_POST['gwd_client_ads_txt']=='Y') ? 'Y' : 'N';
$gwd_client_ads_txt_content = esc_html($_POST['gwd_client_ads_txt_content']);
$gwd_instagram_access_token = $_POST['gwd_instagram_access_token'];
@@ -716,9 +710,6 @@
update_option('gwd_enable_tags_pages',$gwd_enable_tags_pages);
update_option('gwd_maintenance_mode',$gwd_maintenance_mode);
update_option('gwd_maintenance_mode_url',$gwd_maintenance_mode_url);
- update_option('gwd_auto_login',$gwd_auto_login);
- update_option('gwd_auto_login_user',$gwd_auto_login_user);
- update_option('gwd_auto_login_url',$gwd_auto_login_url);
update_option('gwd_client_ads_txt',$gwd_client_ads_txt);
update_option('gwd_client_ads_txt_content',$gwd_client_ads_txt_content);
update_option('gwd_instagram_access_token',$gwd_instagram_access_token);
@@ -780,7 +771,6 @@
</tr>
<tr>
<td><div class="gwd_shadow_box"><h2>Admin Settings</h2>
- <fieldset><label for="gwd_auto_login" class="gwd_norm_label"><input name="gwd_auto_login" type="checkbox" id="gwd_auto_login" value="Y"'.(($gwd_auto_login=='Y') ? ' CHECKED' : '').'> Auto Login User</label><div id="gwd_auto_login_user">Username:<br><input class="long_text" name="gwd_auto_login_user" type="text" value="'.$gwd_auto_login_user.'"><div>Auto Login URL:<br><input class="long_text" name="gwd_auto_login_url" type="text" value="'.$gwd_auto_login_url.'"></div></div></fieldset>
<fieldset>Custom logo for login page:<br><label for="gwd_login_logo" class="gwd_norm_label"><input class="long_text" name="gwd_login_logo" type="text" id="gwd_login_logo" value="'.sanitize_url($gwd_login_logo).'"></label></fieldset>
<fieldset><label for="gwd_remove_site_health" class="gwd_norm_label"><input name="gwd_remove_site_health" type="checkbox" id="gwd_remove_site_health" value="Y"'.(($gwd_remove_site_health=='Y') ? ' CHECKED' : '').'> Disable site health display</fieldset>
<fieldset><label for="gwd_cloudflare_real_ip" class="gwd_norm_label"><input name="gwd_cloudflare_real_ip" type="checkbox" id="gwd_cloudflare_real_ip" value="Y"'.(($gwd_cloudflare_real_ip=='Y') ? ' CHECKED' : '').'> Get real IP if using Cloudflare</fieldset>
@@ -807,8 +797,6 @@
.gwd_small_field_mess{padding-left:10px;font-size:12px;}
.gwd_small_field_mess label{display:inline-block !important;font-weight:normal !important;}
#gwd_maintenance_mode_url{padding:5px 0px 0px 25px;'.(($gwd_maintenance_mode=='Y') ? '' : 'display:none;').'}
- #gwd_auto_login_user{padding:5px 0px 0px 25px;'.(($gwd_auto_login=='Y') ? '' : 'display:none;').'}
- #gwd_auto_login_user div{padding-top:10px}
'.(($gwd_wpform_spam_filters_apply_custom=='Y') ? '' : '#gwd_custom_filters{display:none;}').'
#gwd_custom_filters textarea{width:300px;max-width:100%;height:130px;}
'.(($gwd_client_ads_txt=='Y') ? '' : '#gwd_client_ads_txt_content_div{display:none;}').'
@@ -822,12 +810,6 @@
if(jQuery('#gwd_maintenance_mode').prop('checked')) { jQuery('#gwd_maintenance_mode_url').show(); }
else { jQuery('#gwd_maintenance_mode_url').hide(); }
});
- // AUTO LOGIN USER
- jQuery('#gwd_auto_login').change(function()
- {
- if(jQuery('#gwd_auto_login').prop('checked')) { jQuery('#gwd_auto_login_user').show(); }
- else { jQuery('#gwd_auto_login_user').hide(); }
- });
// APPLY CUSTOM FILTERS
jQuery('#gwd_wpform_spam_filters_apply_custom').change(function() { gwdApplyCustomFilters(); });
function gwdApplyCustomFilters()
@@ -1003,25 +985,28 @@
###################
function gwd_instagram_token()
{
- if(isset($_GET['gwd_ig_token']))
+ if(is_admin() && isset($_POST['gwd_ig_token']))
{
###############
# Define Vars #
###############
- $gwd_instagram_access_token = $_GET['gwd_ig_token'];
- $key = $_GET['key'];
+ $gwd_instagram_access_token = $_POST['gwd_ig_token'];
+ $key = $_POST['key'];
$gwd_client_api_key = get_option('gwd_client_api_key');
##############
# Save Token #
##############
if($key==$gwd_client_api_key) { update_option('gwd_instagram_access_token',$gwd_instagram_access_token); }
-
+ }
+
############
# Redirect #
############
+ if($_SERVER['REQUEST_URI']=='/ig-handoff/')
+ {
if(is_admin() && current_user_can('administrator')) { wp_redirect(get_admin_url().'admin.php?page=gwd_wp_plugin_settings&instagram_token_saved=Y'); }
- elseif($key==$gwd_client_api_key) { include_once('includes/instagram-token-saved.html'); die(); }
+ else { include_once('includes/instagram-token-saved.html'); die(); }
}
}
add_action('init','gwd_instagram_token');
@@ -1047,17 +1032,18 @@
$api_url = "https://graph.instagram.com/v12.0/me/media?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp&access_token={$gwd_instagram_access_token}";
// Fetch data from Instagram API
- $response = file_get_contents($api_url);
- $data = json_decode($response);
+ $response = wp_remote_get($api_url,array('timeout'=>15));
+ if(!is_wp_error($response)) { $data = json_decode(wp_remote_retrieve_body($response)); }
// Check if the data was retrieved successfully
- if ($data && isset($data->data)) {
- $media = $data->data;
+ if($data && isset($data->data))
+ {
+ $media = $data->data;
// Loop through the media items and display them
$count = 0;
foreach ($media as $post) {
- $media_item = (substr_count($post->media_url,'.mp4')>0) ? '<video width="100%"><source src="'.$post->media_url.'" type="video/mp4"></video>' : '<img src="' . $post->media_url . '" alt="' . $post->caption . '">';
- $html .= "n".'<div class="instagram_card"><a href="' . $post->permalink . '" target="_blank">'.$media_item.'<!--<div class="instagram_description">' . $post->caption . '</div>--></a></div>';
+ $media_item = (substr_count($post->media_url,'.mp4')>0) ? '<video width="100%"><source src="'.esc_url($post->media_url).'" type="video/mp4"></video>' : '<img src="'.esc_url($post->media_url).'" alt="'.esc_attr($post->caption).'">';
+ $html .= "n".'<div class="instagram_card"><a href="'.esc_url($post->permalink).'" target="_blank">'.$media_item.'<!--<div class="instagram_description">'.esc_html($post->caption).'</div>--></a></div>';
$count++;
if($count==$max) { break; }
}
@@ -1150,25 +1136,6 @@
# Define Vars #
###############
$gwd_maintenance_mode = get_option('gwd_maintenance_mode');
- $gwd_auto_login = get_option('gwd_auto_login');
- $gwd_auto_login_user = get_option('gwd_auto_login_user');
- $gwd_auto_login_url = get_option('gwd_auto_login_url');
-
- ##############
- # Auto Login #
- ##############
- if(!is_user_logged_in() && !is_admin() && $gwd_auto_login=='Y' && $_SERVER['REQUEST_URI']==$gwd_auto_login_url)
- {
- $user = get_user_by('login', $gwd_auto_login_user);
- if($user)
- {
- wp_set_current_user($user->ID, $user->user_login);
- wp_set_auth_cookie($user->ID);
- do_action('wp_login', $user->user_login);
- wp_redirect(home_url());
- die();
- }
- }
####################
# Maintenance Mode #
@@ -1206,14 +1173,16 @@
################
# Ads.txt File #
################
-if($_SERVER['REQUEST_URI']=='/ads.txt')
+if($_SERVER['REQUEST_URI']==='/ads.txt')
{
$gwd_client_ads_txt = get_option('gwd_client_ads_txt');
- $gwd_client_ads_txt_content = get_option('gwd_client_ads_txt_content');
if($gwd_client_ads_txt=='Y')
{
+ $gwd_client_ads_txt_content = get_option('gwd_client_ads_txt_content');
header("HTTP/1.1 200 OK");
- echo $gwd_client_ads_txt_content;
+ header('Content-Type: text/plain; charset=utf-8');
+ header('X-Content-Type-Options: nosniff');
+ echo trim(wp_strip_all_tags($gwd_client_ads_txt_content));
die();
}
}
@@ -1532,7 +1501,7 @@
add_action('init',function()
{
$gwd_cloudflare_real_ip = get_option('gwd_cloudflare_real_ip');
- if($gwd_cloudflare_real_ip=='Y' && isset($_SERVER['HTTP_CF_CONNECTING_IP']))
+ if($gwd_cloudflare_real_ip=='Y' && filter_var($_SERVER['HTTP_CF_CONNECTING_IP'],FILTER_VALIDATE_IP))
{
$_SERVER['REMOTE_ADDR'] = sanitize_text_field( $_SERVER['HTTP_CF_CONNECTING_IP'] );
}