CVE-2026-6712

Website LLMs.txt <= 8.2.6 - Authenticated (Admin+) Stored Cross-Site Scripting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
4.4
CVSS Score
4.4
CVSS Score
medium
Severity
8.2.7
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
High
Privileges Required
High
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=8.2.6
PublishedApril 20, 2026
Last updatedApril 21, 2026
Affected pluginwebsite-llms-txt

What Changed in the Fix

Changes introduced in v8.2.7

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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.php at 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_html is disabled (e.g., Multisite).

3. Code Flow

  1. Entry Point (Input): The user submits the settings form in the admin dashboard. The form targets options.php (standard WordPress Settings API).
  2. Persistence: WordPress saves the llms_generator_settings array into the wp_options table. Due to missing or insufficient sanitization callbacks during register_setting (inferred), malicious HTML tags/attributes are stored.
  3. Processing: The plugin loads settings using get_option('llms_generator_settings') inside the LLMS_Generator constructor (includes/class-llms-generator.php, line 24).
  4. Sink (Output): When an admin visits the settings page, admin/admin-page.php iterates through the available post types.
  5. 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 value attribute without using esc_attr(). This allows an attacker to break out of the attribute using "> and inject a script.

4. Nonce Acquisition Strategy

The settings form uses the WordPress Settings API, which requires a nonce generated for the llms_generator_settings-options action.

  1. Identify Page: Navigate to the settings page /wp-admin/options-general.php?page=llms-generator (or use browser_navigate to find the correct options-general.php submenu link).
  2. Extract Nonce: Since we are performing an authenticated exploit as Admin, we use the browser_eval tool to extract the nonce from the hidden input field generated by settings_fields('llms_generator_settings').
  3. 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

  1. Login: Authenticate as an Administrator.
  2. Access Settings: Navigate to the LLMs.txt settings page to retrieve the required nonces and verify the correct post type labels (usually "Posts").
  3. Payload Construction:
    • Target Parameter: llms_generator_settings[post_name][Posts] (assuming the "Post" type has the label "Posts").
    • Payload: "><script>alert(document.domain)</script>
  4. Submit Exploit: Send a POST request to /wp-admin/options.php using the http_request tool.
  5. 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_settings
    • action: update
    • _wpnonce: [EXTRACTED_NONCE]
    • _wp_http_referer: /wp-admin/options-general.php?page=llms-generator
    • llms_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

  1. Plugin Activation: Ensure website-llms-txt is installed and activated.
  2. User Creation: Ensure an Administrator user exists.
  3. Target Post Type: Ensure the default "Post" type exists (standard in WP).
  4. (Optional) Restriction: To prove the severity, disable unfiltered_html for admins in wp-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

  1. Database Check: Use WP-CLI to verify the payload is stored in the options table:
    wp option get llms_generator_settings --format=json
    
    Check if the post_name object contains the script tag for the "Posts" key.
  2. Source Inspection: Use http_request to 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.php that use $settings. For example, llms_txt_title or llms_txt_description. If they are also rendered without escaping, they serve as alternative injection points.
    • Potential sink: llms_txt_title field at admin/admin-page.php:191 (inferred location in settings form).
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/website-llms-txt/8.2.6/admin/admin-page.php /home/deploy/wp-safety.org/data/plugin-versions/website-llms-txt/8.2.7/admin/admin-page.php
--- /home/deploy/wp-safety.org/data/plugin-versions/website-llms-txt/8.2.6/admin/admin-page.php	2026-02-11 11:25:06.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/website-llms-txt/8.2.7/admin/admin-page.php	2026-03-13 18:44:40.000000000 +0000
@@ -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.