Contents

CVE-2025-58674 Analysis & POC

A Stored Cross Site Scripting (XSS) vulnerability occurs in WordPress Core prior to version 6.8.3. The root cause is improper input handling when generating dynamic pages, affecting the menu creation feature (nav menus).

  • CVE ID: CVE-2025-58674
  • Vulnerability Type: Cross Site Scripting (XSS)
  • Affected Versions: <= 6.8.2
  • Patched Versions: 6.8.3
  • CVSS severity: Low (5.9)
  • Required Privilege: Author
  • Product: WordPressCore
  • Local WordPress & Debugging: Local WordPress and Debugging.
  • WordPress Core Versions: v6.8.2 (vulnerable) and v6.8.3 (patched).
  • Diff Tool - Meld or any diff/comparison tool to inspect and compare differences between the two versions.
  • Theme - Astra: A very popular theme among WordPress users, which supports quick creation of nav menu items.

WordPress is open source and its repository is on GitHub, so we can look at the commit related to the XSS fix to observe the changes and understand where the vulnerability occurs.

Vulnerable version:

updateParentDropdown : function() {
    return this.each(function(){
        var menuItems = $( '#menu-to-edit li' ),
            parentDropdowns = $( '.edit-menu-item-parent' );

        $.each( parentDropdowns, function() {
            var parentDropdown = $( this ),
                $html = '',
                $selected = '',
                currentItemID = parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-db-id' ).val(),
                currentparentID = parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-parent-id' ).val(),
                currentItem = parentDropdown.closest( 'li.menu-item' ),
                currentMenuItemChild = currentItem.childMenuItems(),
                excludeMenuItem = [ currentItemID ];

            if ( currentMenuItemChild.length > 0 ) {
                $.each( currentMenuItemChild, function(){
                    var childItem = $(this),
                        childID = childItem.find( '.menu-item-data-db-id' ).val();

                    excludeMenuItem.push( childID );
                });
            }

            if ( currentparentID == 0 ) {
                $selected = 'selected';
            }

            $html += '<option ' + $selected + ' value="0">' + wp.i18n._x( 'No Parent', 'menu item without a parent in navigation menu' ) + '</option>';

            $.each( menuItems, function() {
                var menuItem = $(this),
                $selected = '',
                menuID = menuItem.find( '.menu-item-data-db-id' ).val(),
                menuTitle = menuItem.find( '.edit-menu-item-title' ).val();

                if ( ! excludeMenuItem.includes( menuID ) ) {
                    if ( currentparentID == menuID ) {
                        $selected = 'selected';
                    }
                    $html += '<option ' + $selected + ' value="' + menuID + '">' + menuTitle + '</option>';
                }
            });

            parentDropdown.html( $html );
        });
        
    });
},

In the vulnerable version, the value menuTitle is inserted into the <option> tag and rendered into HTML using jQuery’s html() method without any XSS prevention. The html() function replaces the HTML content inside the element, so if menuTitle contains malicious code it will be executed in the browser.

Patched version:

updateParentDropdown : function() {
    return this.each(function(){
        var menuItems = $( '#menu-to-edit li' ),
            parentDropdowns = $( '.edit-menu-item-parent' );

        $.each( parentDropdowns, function() {
            var parentDropdown = $( this ),
                currentItemID = parseInt( parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-db-id' ).val() ),
                currentParentID = parseInt( parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-parent-id' ).val() ),
                currentItem = parentDropdown.closest( 'li.menu-item' ),
                currentMenuItemChild = currentItem.childMenuItems(),
                excludeMenuItem =  /** @type {number[]} */ [ currentItemID ];

            parentDropdown.empty();

            if ( currentMenuItemChild.length > 0 ) {
                $.each( currentMenuItemChild, function(){
                    var childItem = $(this),
                        childID = parseInt( childItem.find( '.menu-item-data-db-id' ).val() );

                    excludeMenuItem.push( childID );
                });
            }

            parentDropdown.append(
                $( '<option>', {
                    value: '0',
                    selected: currentParentID === 0,
                    text: wp.i18n._x( 'No Parent', 'menu item without a parent in navigation menu' ),
                } )
            );

            $.each( menuItems, function() {
                var menuItem = $(this),
                menuID = parseInt( menuItem.find( '.menu-item-data-db-id' ).val() ),
                menuTitle = menuItem.find( '.edit-menu-item-title' ).val();

                if ( ! excludeMenuItem.includes( menuID ) ) {
                    parentDropdown.append(
                        $( '<option>', {
                            value: menuID.toString(),
                            selected: currentParentID === menuID,
                            text: menuTitle,
                        } )
                    );
                }
            });
        });
        
    });
},

The patch fixes the issue by explicitly assigning menuTitle to the text property instead of injecting it into HTML. This ensures menuTitle is treated as plain text and cannot contain or execute malicious JavaScript. Thus, data added into the <option> element is safe and the XSS vector via menuTitle is eliminated.

Comparison between vulnerable and patched versions

Comparison between vulnerable and patched versions

Inspecting the code in the browser shows menuItems = #menu-to-edit li is an array of <li> elements inside the <ul> with id=menu-to-edit

Inspecting code in the browser

Inspecting code in the browser

The updateParentDropdown function iterates over the <li> elements, retrieves the value of the <input> with class edit-menu-item-title, assigns that value to menuTitle inside an <option> tag, and renders it into HTML.

Inspecting DOM after adding a menu

Inspecting DOM after adding a menu

In the UI:

Display in the admin UI

Display in the admin UI

In the commit there is a change that seems useful but did not help the analysis.

HTML entity encode (1)

HTML entity encode (1)

I used // to comment out all lines related to html_entity_decode of origin_title but was still able to exploit. Previously I had set debug points at those locations but nothing happened.

Besides commenting them out, I selected the menu containing the XSS payload and clicked Add to Menu, capturing the request with Burp Suite to see whether the added value was HTML entity encoded.

HTML entity encode (2)

HTML entity encode (2)

menu-item-title is the value taken from the input named menu-item[-5][menu-item-title] derived from the post-title, which is checked to add into the request body

<input type="hidden" class="menu-item-title" name="menu-item[-5][menu-item-title]" value="<script>alert(document.domain)</script>">

One interesting thing here: the browser takes the decoded HTML entity value to add into the request, meaning the value from the server was encoded before being echoed into HTML.

Browser decodes HTML entity before sending

Browser decodes HTML entity before sending

👉 The browser decoded the HTML entity before rendering the HTML.

On the DOM, after clicking Add to Menu => attachTabsPanelListeners is called and appends the selected value to the bottom of the menu item list.

Appending the item to the end of the menu list

Appending the item to the end of the menu list

👉 edit-menu-item-title contains the XSS payload. The updateParentDropdown function will take it and assign it to an <option> element => XSS occurs

  • Source: post-title — the post title
  • Sink: parentDropdown.html( $html ) which may contain malicious HTML
$html += '<option ' + $selected + ' value="' + menuID + '">' + menuTitle + '</option>';
  • Use an Author account to create a post with a title containing an XSS payload
  • An Admin visits the endpoint wp-admin/nav-menus.php and adds menus from the created post
  • The JavaScript event is triggered
Proof of Concept: XSS

Proof of Concept: XSS

The <script> tag inside an <option> within a <select> can be executed, while other tags inside <option> are not — they only return the text inside. {: .prompt-info }

  • When the browser parses the HTML string, the <script> is not truly inside the <option> flow; the parser “lifts” the script out of the <option> and inserts it into the DOM tree.
<select name="" id="">
    <option value="">abc</option>
    <option value=""><script>alert(1)</script></option>
</select>
  • Therefore, the script executes immediately, while the <option> remains but its content is empty or displays nothing.
Inspecting DOM after the script is lifted

Inspecting DOM after the script is lifted

  • For other tags
<select name="" id="">
	<option value="">abc</option>
	<option value=""><b>abd</b></option>
</select>
  • The browser follows the spec: <option> is text-only.
  • When parsing <b> or <img> inside an <option>:
    • <b> is treated as text => the tag is removed, only “abd” is displayed as text.
    • <img> is removed entirely.
Inspect: rendering option with inline elements

Inspect: rendering option with inline elements

CVE-2025-58674 demonstrates the risk of inserting user-controlled data directly into HTML via html() or innerHTML. A <script> inside an <option> can be lifted out by the parser and executed, while other tags are stripped or treated as text. The patch uses the text property to ensure safety and eliminate XSS.

  • Do not concatenate HTML strings from user data and insert them with html()/innerHTML.
  • Use safe element creation APIs (option.text, document.createElement).
  • Always escape/sanitize data both server-side and client-side.
  • Understand parser behavior: <script> can escape an inert container.

Cross-site scripting (XSS) cheat sheet — PortSwigger

WordPress Core <= 6.8.2 is vulnerable to Cross Site Scripting (XSS)

Related Content