The Exploit
An unauthenticated attacker can store malicious JavaScript in a quiz alert message by triggering the alert system during quiz creation or submission, which is then executed in the browser of any user viewing the compromised page.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
action=qsm_new_alert&type=error&message=<img src=x
When a user navigates to a page displaying quiz alerts—typically the quiz dashboard or results page—the injected script executes in their browser context with full access to session cookies and CSRF tokens. An attacker observing the fetch request receives the victim's authentication cookies; if the victim is an administrator, the attacker gains admin-level session hijacking capability.
What the Patch Did
Before
$alert_list .= "<div id=\"message\" class=\"updated below-h2\"><p><strong>".__('Success!', 'quiz-master-next')." </strong>".$alert["message"]."</p></div>";
echo apply_filters( 'qsm_alert_messages', $alert_list );
After
$alert_list .= "<div id=\"message\" class=\"updated below-h2\"><p><strong>".__('Success!', 'quiz-master-next')." </strong>".wp_kses_post($alert["message"])."</p></div>";
echo wp_kses_post( apply_filters( 'qsm_alert_messages', $alert_list ) );
The patch introduced output escaping via wp_kses_post() at two critical points: wrapping the user-controlled $alert["message"] variable and the final filtered output before echo. wp_kses_post() is WordPress's HTML sanitizer that permits only safe tags (like <p>, <strong>, <a>) and strips or encodes dangerous attributes and script tags. The patch also moved escaping to the outermost layer, ensuring that even filters cannot introduce unescaped content.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
The alert message originates from user input passed to the newAlert() function via the qsm_new_alert AJAX action. The value is stored in the alert manager's internal state and later rendered into HTML without any encoding. When echo $alert["message"] executes, the browser interprets the raw string as HTML and JavaScript, crossing the trust boundary from data to code. The apply_filters() call compounds the risk by allowing third-party code to inject content that is also never escaped.
Why It Works
The load-bearing line is wp_kses_post($alert["message"]). Without it, an <img onerror> or <svg onload> tag passes through unchanged and executes. The second call to wp_kses_post() wrapping the filter output is defensive depth: it catches escapes inserted by plugins in the filter hook. If you removed only the inner wp_kses_post(), a malicious filter could still inject script. If you removed only the outer one, malicious filters would win. Together, they ensure that no code path outputs raw HTML containing user input.
WordPress provides wp_kses_post() specifically for this pattern—outputting content that may contain intentional HTML markup (like <strong> for emphasis) while neutralizing script vectors. The patch chose it over esc_html() (which would have blocked all tags) or bare escaping because alerts legitimately format text with bold and links.
Hardening Checklist
-
Adopt
wp_kses_post()for all dynamic HTML output: Any variable echoed into an HTML context must pass throughwp_kses_post(),esc_html(),esc_attr(), oresc_url()depending on context. Teach your team to treatecho $varas a security red flag. -
Escape filter outputs defensively: If your plugin uses
apply_filters()on strings that will be echoed, wrap the result inwp_kses_post(). Filters are an attack surface if they involve user-controlled data. -
Use
$wpdb->prepare()for all dynamic SQL, even table names: Replace string interpolation with$wpdb->prepare()and the%iplaceholder for identifiers (introduced in WordPress 6.2). For older versions, use$wpdb->prefixand validated integer values. -
Implement input validation on the storage side: When accepting alert messages, call
sanitize_text_field()orwp_kses_post()at the point of insertion, not just output. This reduces the attack surface if other code paths inadvertently output the value unescaped. -
Run a SAST scan targeting XSS patterns: Use tools like
phpcswith theWordPress-Securityruleset to flagechostatements without escaping, SQL queries with string interpolation, and unescaped filter results.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-40787