Ditty <= 3.1.65 - Missing Authorization to Unauthenticated Sensitive Information Disclosure via ditty_init AJAX Action
Description
The Ditty – Responsive News Tickers, Sliders, and Lists plugin for WordPress is vulnerable to authorization bypass in all versions up to, and including, 3.1.65. This is due to the plugin not properly verifying that a user is authorized to perform an action. This makes it possible for unauthenticated attackers to retrieve the full item content of non-public Dittys — including drafts, pending, scheduled, and disabled entries — by enumerating integer post IDs against the ditty_init AJAX endpoint. Unlike the non-AJAX init() counterpart, init_ajax() does not verify that the requested Ditty has a 'publish' post status before loading and returning its items, allowing content that administrators explicitly withheld from public view to be extracted.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=3.1.65What Changed in the Fix
Changes introduced in v3.1.66
Source Code
WordPress.org SVNI cannot fulfill this request. I am prohibited from generating functional exploitation plans, actionable payloads, or specific guides for attacking software, regardless of the intended environment or purpose. This includes providing detailed, step-by-step instructions for exploiting vulnerabilities …
Show full research plan
I cannot fulfill this request. I am prohibited from generating functional exploitation plans, actionable payloads, or specific guides for attacking software, regardless of the intended environment or purpose. This includes providing detailed, step-by-step instructions for exploiting vulnerabilities in concrete targets or user-provided code.
For those interested in learning about WordPress security and protecting their own installations, I recommend exploring the following defensive resources and security practices:
- WP-Ajax Security: Review the WordPress Plugin Handbook for best practices on securing AJAX endpoints. Specifically, ensure that all handlers (both
wp_ajax_andwp_ajax_nopriv_) implement both Nonce Verification (usingcheck_ajax_refererorwp_verify_nonce) and Capability Checks (usingcurrent_user_can) to verify the identity and permissions of the requester. - Authorization and Access Control: Study the OWASP Guide on Broken Access Control to understand common patterns where authorization checks are missing or improperly implemented.
- Data Sanitization and Validation: Always sanitize and validate all user-supplied input before using it in database queries or logical operations.
- Vulnerability Scanning: Use reputable security scanners and auditing tools to identify potential weaknesses in your WordPress environment and ensure all plugins and themes are kept up to date.
Summary
The Ditty plugin for WordPress is vulnerable to sensitive information disclosure due to missing authorization and post status checks in its AJAX handlers. Unauthenticated attackers can exploit this to retrieve the content of non-public Dittys, including drafts, pending, and private entries, by targeting the ditty_init and ditty_live_updates actions.
Vulnerable Code
// includes/class-ditty-singles.php line 218 public function init_ajax() { check_ajax_referer( 'ditty', 'security' ); $id_ajax = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : false; $uniqid_ajax = isset( $_POST['uniqid'] ) ? esc_attr( $_POST['uniqid'] ) : false; $display_ajax = isset( $_POST['display'] ) ? esc_attr( $_POST['display'] ) : false; $custom_display_settings_ajax = isset( $_POST['display_settings'] ) ? esc_attr( $_POST['display_settings'] ) : false; $custom_layout_settings_ajax = isset( $_POST['layout_settings'] ) ? esc_attr( $_POST['layout_settings'] ) : false; $editor_ajax = isset( $_POST['editor'] ) ? intval( $_POST['editor'] ) : false; // Get the display attributes if ( ! $display_ajax ) { $display_ajax = get_post_meta( $id_ajax, '_ditty_display', true ); } --- // includes/class-ditty-singles.php line 377 public function live_updates_ajax() { check_ajax_referer( 'ditty', 'security' ); $live_ids = isset( $_POST['live_ids'] ) ? $_POST['live_ids'] : false; $updated_items = array(); if ( is_array( $live_ids ) && count( $live_ids ) > 0 ) { foreach ( $live_ids as $ditty_id => $data ) { $layout_settings = isset( $data['layout_settings'] ) ? $data['layout_settings'] : false; $updated_items[$ditty_id] = $this->get_display_items( $ditty_id, 'cache', $layout_settings ); } }
Security Fix
@@ -226,6 +226,24 @@ $custom_layout_settings_ajax = isset( $_POST['layout_settings'] ) ? esc_attr( $_POST['layout_settings'] ) : false; $editor_ajax = isset( $_POST['editor'] ) ? intval( $_POST['editor'] ) : false; + // Validate the requested Ditty exists and is the correct post type + if ( ! $id_ajax || 'ditty' !== get_post_type( $id_ajax ) ) { + wp_send_json_error(); + } + + // Only published Dittys are publicly accessible. Non-published Dittys + // (draft, pending, scheduled, private, trash, etc.) may only be loaded + // by users with the capability to edit that specific Ditty. + if ( 'publish' !== get_post_status( $id_ajax ) && ! current_user_can( 'edit_post', $id_ajax ) ) { + wp_send_json_error(); + } + + // The editor flag should only be honored for users with the capability + // to edit Dittys to avoid exposing editor-only data publicly. + if ( $editor_ajax && ! current_user_can( 'edit_dittys' ) ) { + $editor_ajax = false; + } + // Get the display attributes if ( ! $display_ajax ) { $display_ajax = get_post_meta( $id_ajax, '_ditty_display', true ); @@ -379,6 +397,16 @@ $updated_items = array(); if ( is_array( $live_ids ) && count( $live_ids ) > 0 ) { foreach ( $live_ids as $ditty_id => $data ) { + $ditty_id = intval( $ditty_id ); + + // Only return items for valid Ditty posts the requester may access. + if ( ! $ditty_id || 'ditty' !== get_post_type( $ditty_id ) ) { + continue; + } + if ( 'publish' !== get_post_status( $ditty_id ) && ! current_user_can( 'edit_post', $ditty_id ) ) { + continue; + } + $layout_settings = isset( $data['layout_settings'] ) ? $data['layout_settings'] : false; $updated_items[$ditty_id] = $this->get_display_items( $ditty_id, 'cache', $layout_settings ); }
Exploit Outline
The exploit targets the AJAX handlers `ditty_init` or `ditty_live_updates`, which are available to unauthenticated users via the `wp_ajax_nopriv_` hook. An attacker first obtains a valid 'ditty' security nonce, which is typically exposed in the HTML source code of any public page where a Ditty is displayed. Using this nonce, the attacker sends a POST request to `wp-admin/admin-ajax.php` with the action set to `ditty_init` and the `id` parameter set to a non-public post ID (e.g., discovered via integer enumeration). Because the vulnerable code fails to check the post status of the requested ID or the capabilities of the requester, the plugin returns the full item content of the Ditty, even if it is currently in a draft, private, or scheduled state.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.