Image Widget <= 4.4.11 - Authenticated (Author+) Stored Cross-Site Scripting
Description
The Image Widget plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 4.4.11 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with author-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v4.4.12
Source Code
WordPress.org SVNThis research plan targets a Stored Cross-Site Scripting (XSS) vulnerability in the **Image Widget** plugin (<= 4.4.11). The vulnerability allows an authenticated user with **Author** privileges to inject malicious scripts into the site via media metadata, which is subsequently rendered by the widge…
Show full research plan
This research plan targets a Stored Cross-Site Scripting (XSS) vulnerability in the Image Widget plugin (<= 4.4.11). The vulnerability allows an authenticated user with Author privileges to inject malicious scripts into the site via media metadata, which is subsequently rendered by the widget without proper escaping.
1. Vulnerability Summary
- Vulnerability: Authenticated Stored XSS.
- Location:
Tribe_Image_Widgetclass (main file) andresources/js/image-widget.js. - Cause: The plugin fails to sanitize media metadata (Alt Text, Description) when importing it into a widget instance and fails to escape this data during output in both the Admin preview and the Frontend rendering.
- Role Requirement: Author (or higher). Authors can upload and edit their own media, which serves as the injection vector.
2. Attack Vector Analysis
- Injection Point: Media Library attachment metadata (Title, Alt Text, or Description).
- Transport: The metadata is "pulled" into the widget settings via JavaScript (
image-widget.js) when an administrator selects an image for the widget. - Sink (Admin): The
imageWidget.imgHTML()function inimage-widget.jsappends thealttext directly to an HTML string which is then injected into the DOM using.html(). - Sink (Frontend): The
widget()method inimage-widget.phpprocesses the stored fields. If the template (views/widget.php) echoes these fields without escaping, the XSS executes on the frontend.
3. Code Flow
- Author Action: Uploads an image and updates its "Alternative Text" to include a breakout payload (e.g.,
"><img src=x onerror=alert(1)>). - Admin Action: Navigates to
wp-admin/widgets.phpand adds an "Image Widget". - Selection: Admin clicks "Select Image". The
uploaderfunction inimage-widget.jsopens the WP Media Manager. - Import: Upon selecting the Author's image,
render(widget_id, widget_id_string, attachment)is triggered. - Admin Execution (Preview):
rendercallsimgHTML(attachment).imgHTMLconstructs:img_html += 'alt="' + attachment.alt + '" ';(Vulnerable Sink).$("#" + widget_id_string + 'preview').html(img_html);executes the payload immediately in the Admin's browser.
- Persistence:
renderalso populates the widget form:imageWidget.updateInputIfEmpty(..., 'alt', attachment.alt). - Storage: When the Admin clicks "Save", the payload is stored in the widget's instance data in the
wp_optionstable. - Frontend Execution:
Tribe_Image_Widget::widget()retrieves the instance data and renders it.
4. Nonce Acquisition Strategy
To inject the payload as an Author, we must update a media attachment. This requires a nonce for the save-attachment AJAX action.
- Identify Attachment: Upload an image as an Author and get its
ATTACHMENT_ID. - Navigate to Edit Page: Use
browser_navigatetowp-admin/post.php?post=ATTACHMENT_ID&action=edit. - Extract Nonce:
- Use
browser_eval("document.querySelector('#_wpnonce').value")to get the nonce from the edit form. - Alternatively, look for the
wp.mediasettings in the localized JS.
- Use
5. Exploitation Strategy
Step 1: Payload Preparation (As Author)
Upload an image and update its metadata with the payload.
- Request Type:
POSTto/wp-admin/admin-ajax.php - Parameters:
action:save-attachmentid:ATTACHMENT_IDnonce:[EXTRACTED_NONCE]changes[image_alt]:"><img src=x onerror=alert(document.domain)>changes[description]:<script>alert('Stored XSS')</script>
Step 2: Trigger Admin Execution
The agent must simulate an Admin selecting the image.
- Navigate to
wp-admin/widgets.phpas Admin. - Add an "Image Widget".
- Use
browser_clickon the "Select Image" button. - Select the Author's image in the Media Library.
- Observe: The
imgHTMLfunction will execute thealtpayload in the widget's preview area.
Step 3: Persistence (Frontend)
- Complete the widget setup and click "Save".
- Navigate to the site's homepage (or wherever the sidebar is active).
- Observe: The payload executes for all visitors.
6. Test Data Setup
- User: Create an Author user.
wp user create attacker attacker@example.com --role=author --user_pass=password - Media: Upload an image as the Author.
wp media import https://wordpress.org/screenshot.png --user=attacker - Note ID: Capture the resulting Attachment ID (e.g.,
123).
7. Expected Results
- Admin Side: An alert box should trigger the moment the image is selected in the widget settings due to the
altattribute breakout inimage-widget.js. - Frontend Side: The page source for the widget will contain the unescaped payload:
<img ... alt=""><img src=x onerror=alert(document.domain)>" />
8. Verification Steps
- Check Database:
Confirm the payload exists in the serialized widget settings.wp option get widget_widget_sp_image - Check HTML Output:
Usehttp_requestto fetch the homepage and grep for the payload in thewidget_sp_imagecontainer.
9.
Summary
The Image Widget plugin for WordPress is vulnerable to Authenticated Stored Cross-Site Scripting (XSS) in versions up to 4.4.11. This occurs because the plugin fails to sanitize and escape media metadata such as 'Alternative Text' when importing images into a widget, allowing authors to inject malicious scripts that execute in the context of an administrator or site visitor.
Vulnerable Code
// resources/js/image-widget.js // Lines 84-93: String concatenation without escaping imgHTML : function( attachment ) { var img_html = '<img src="' + attachment.url + '" '; img_html += 'width="' + attachment.width + '" '; img_html += 'height="' + attachment.height + '" '; if ( attachment.alt != '' ) { img_html += 'alt="' + attachment.alt + '" '; } img_html += '/>'; return img_html; }, --- // resources/js/image-widget.js // Line 37: Injection into the DOM via .html() $("#" + widget_id_string + 'preview').html(imageWidget.imgHTML( attachment ));
Security Fix
@@ -70,13 +70,20 @@ } }, + // Escape a string for safe use inside an HTML attribute. + escAttr : function( str ) { + var div = document.createElement( 'div' ); + div.appendChild( document.createTextNode( str ) ); + return div.innerHTML.replace( /"/g, '"' ).replace( /'/g, ''' ); + }, + // Render html for the image. imgHTML : function( attachment ) { - var img_html = '<img src="' + attachment.url + '" '; - img_html += 'width="' + attachment.width + '" '; - img_html += 'height="' + attachment.height + '" '; + var img_html = '<img src="' + imageWidget.escAttr( attachment.url ) + '" '; + img_html += 'width="' + imageWidget.escAttr( String( attachment.width ) ) + '" '; + img_html += 'height="' + imageWidget.escAttr( String( attachment.height ) ) + '" '; if ( attachment.alt != '' ) { - img_html += 'alt="' + attachment.alt + '" '; + img_html += 'alt="' + imageWidget.escAttr( attachment.alt ) + '" '; } img_html += '/>'; return img_html;
Exploit Outline
1. An authenticated attacker with Author privileges uploads an image to the WordPress Media Library. 2. The attacker modifies the image's 'Alternative Text' or 'Description' metadata to include an XSS payload (e.g., "><img src=x onerror=alert(1)>). 3. An administrator navigates to the 'Widgets' screen (wp-admin/widgets.php) and adds or edits an 'Image Widget'. 4. When the administrator clicks 'Select Image' and chooses the malicious image uploaded by the Author. 5. The plugin's JS function 'imgHTML' constructs an HTML string using the unescaped payload and injects it into the preview area via the jQuery .html() method, triggering the exploit in the admin's session. 6. Upon saving the widget, the payload is persisted and will execute in the browsers of all visitors who view the page containing the widget.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.