[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$feEC9BSeO-TQkBnzsusYOIjdMl4TQADHRmIm6rJ4H7qY":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":27,"research_verified":28,"research_rounds_completed":29,"research_plan":30,"research_summary":31,"research_vulnerable_code":32,"research_fix_diff":33,"research_exploit_outline":34,"research_model_used":35,"research_started_at":36,"research_completed_at":37,"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":28,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":28,"source_links":38},"CVE-2026-9015","equalize-digital-accessibility-checker-missing-authorization-to-authenticated-subscriber-arbitrary-accessibility-issue-m","Equalize Digital Accessibility Checker \u003C= 1.42.0 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Accessibility Issue Modification via edac_insert_ignore_data AJAX Action","The Equalize Digital Accessibility Checker – WCAG, ADA, EAA and Section 508 compliance plugin for WordPress is vulnerable to authorization bypass in all versions up to, and including, 1.42.0. This is due to the plugin not properly verifying that a user is authorized to perform an action. This makes it possible for authenticated attackers, with subscriber-level access and above, to modify the ignore state, ignore reason, and ignore comment of arbitrary accessibility issues across the entire site — including mass modification of all rows sharing an 'object' identifier when largeBatch=true is supplied — corrupting accessibility audit integrity by hiding or dismissing findings outside their authorization scope.","accessibility-checker",null,"\u003C=1.42.0","1.42.1","medium",4.3,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:L\u002FUI:N\u002FS:U\u002FC:N\u002FI:L\u002FA:N","Missing Authorization","2026-05-27 18:52:56","2026-05-28 07:43:43",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F613fc64a-1206-4a11-b945-216068b9339a?source=api-prod",1,[22,23,24,25,26],"accessibility-checker.php","admin\u002Fclass-ajax.php","changelog.txt","readme.txt","vendor\u002Fcomposer\u002Finstalled.php","researched",false,3,"# Exploitation Research Plan: CVE-2026-9015\n\n## 1. Vulnerability Summary\nThe **Equalize Digital Accessibility Checker** plugin (up to version 1.42.0) contains a missing authorization vulnerability in its AJAX handler for dismissing or ignoring accessibility issues. While the plugin implements a nonce check (protecting against CSRF), it fails to verify if the authenticated user possesses the necessary capabilities (like `manage_options` or the ability to edit the specific post) to modify accessibility audit data. This allows any authenticated user, including those with **Subscriber** roles, to dismiss accessibility findings site-wide, potentially hiding critical compliance issues.\n\n## 2. Attack Vector Analysis\n- **Endpoint**: `\u002Fwp-admin\u002Fadmin-ajax.php`\n- **Action**: `edac_insert_ignore_data`\n- **Authentication**: Authenticated (Subscriber+)\n- **Parameters**:\n    - `action`: `edac_insert_ignore_data`\n    - `nonce`: A valid nonce for the `ajax-nonce` action string.\n    - `object`: The identifier for the accessibility issue (often a CSS selector or hash).\n    - `ignore_state`: The new state (e.g., `1` for ignored, `0` for active).\n    - `ignore_reason`: The reason for dismissal (e.g., \"Manual Review\").\n    - `ignore_comment`: A text comment explaining the dismissal.\n    - `largeBatch`: (Boolean string) If set to `\"true\"`, the plugin modifies all issue rows sharing the same `object` identifier across the database.\n    - `post_id`: (Required for context) The ID of the post the issue is associated with.\n\n## 3. Code Flow\n1. **Hook Registration**: In `admin\u002Fclass-ajax.php`, the `init_hooks()` method registers the AJAX action:\n   ```php\n   add_action( 'wp_ajax_edac_insert_ignore_data', [ $this, 'add_ignore' ] );\n   ```\n2. **Missing Check**: The `add_ignore` method (inferred from the `summary` method pattern in the same class) likely verifies the nonce using `wp_verify_nonce( $_REQUEST['nonce'], 'ajax-nonce' )`. \n3. **Authorization Failure**: Unlike the `summary()` method which explicitly checks `current_user_can( 'edit_post', $post_id )`, the `add_ignore` method omits this check or uses an insufficient check that allows Subscriber access.\n4. **Data Persistence**: The method processes the `object` and `ignore_state` parameters and updates the plugin's custom database table (likely `wp_edac_issues`) to mark the issue as dismissed.\n\n## 4. Nonce Acquisition Strategy\nThe `ajax-nonce` is used across several EDAC AJAX actions. It is localized for the WordPress admin environment.\n\n1. **Accessibility**: Since Subscribers can access the WordPress dashboard (`\u002Fwp-admin\u002Fprofile.php`), the plugin likely enqueues its admin scripts there if it's not properly restricted.\n2. **Localization Key**: Based on standard plugin patterns and the `summary` function, the nonce is likely localized under a JS object named `edac`, `edac_ajax`, or similar.\n3. **Acquisition Steps**:\n   - Log in as a Subscriber.\n   - Navigate to `\u002Fwp-admin\u002Fprofile.php`.\n   - Execute the following in the browser console (via `browser_eval`):\n     ```javascript\n     \u002F\u002F Search for the localized object containing 'ajax-nonce'\n     \u002F\u002F Verification needed: The actual object name is often edac_params or edac_ajax\n     window.edac_params?.nonce || window.edac_ajax?.nonce || window.edac?.nonce;\n     ```\n   - If not found on the profile page, create a post\u002Fpage with a shortcode if the plugin allows it, but since this is an admin AJAX action, checking `admin-ajax.php` related scripts in the admin header is the primary route.\n\n## 5. Exploitation Strategy\n### Step 1: Discover a Target Issue\nFirst, identify an existing accessibility issue in the database to dismiss. Accessibility issues are tied to an `object` (usually a DOM selector).\n\n### Step 2: Craft the Payload\nConstruct a POST request to `admin-ajax.php`.\n\n**Request Details**:\n- **URL**: `http:\u002F\u002Fvulnerable-site.com\u002Fwp-admin\u002Fadmin-ajax.php`\n- **Method**: `POST`\n- **Content-Type**: `application\u002Fx-www-form-urlencoded`\n- **Body**:\n  ```\n  action=edac_insert_ignore_data&nonce=[NONCE]&post_id=[TARGET_POST_ID]&object=[TARGET_SELECTOR]&ignore_state=1&ignore_reason=Exploit+Test&ignore_comment=Unauthorized+Dismissal&largeBatch=true\n  ```\n\n### Step 3: Execute via http_request\nUse the Playwright-based `http_request` tool to send the authenticated request as the Subscriber.\n\n## 6. Test Data Setup\n1. **Target Content**: Create a post with a deliberate accessibility error (e.g., an image without an `alt` attribute).\n   ```bash\n   wp post create --post_type=post --post_title=\"Vuln Test\" --post_content='\u003Cimg src=\"https:\u002F\u002Fexample.com\u002Ftest.jpg\">' --post_status=publish\n   ```\n2. **Trigger Scan**: Use the plugin's scan functionality (or wait for the auto-scan) so the issue is populated in the database.\n3. **Identify Object**: Check the database for the issue's `object` identifier.\n   ```bash\n   wp db query \"SELECT object, post_id FROM wp_edac_issues LIMIT 1;\"\n   ```\n4. **Attacker User**: Create a Subscriber user.\n   ```bash\n   wp user create attacker attacker@example.com --role=subscriber --user_pass=password123\n   ```\n\n## 7. Expected Results\n- **Response**: The server should return a JSON success message (e.g., `{\"success\": true}`).\n- **Effect**: The accessibility issue identified by the `object` selector will be marked as \"Dismissed\" or \"Ignored\" in the database.\n- **Site Impact**: If `largeBatch=true`, all instances of that specific accessibility issue across the entire site (associated with that `object` selector) will be dismissed.\n\n## 8. Verification Steps\n1. **DB Check**: Verify the `is_ignored` (or similar column name) status in the plugin table.\n   ```bash\n   wp db query \"SELECT * FROM wp_edac_issues WHERE object = '[TARGET_SELECTOR]';\"\n   ```\n   Check if `ignore_reason` matches \"Exploit Test\".\n2. **UI Check**: Log in as Administrator and view the Accessibility Checker \"Dismissed Issues\" log. The unauthorized dismissal should appear there, attributed to the Subscriber user.\n\n## 9. Alternative Approaches\nIf the `object` identifier is a complex hash or hard to find:\n- **Mass Dismissal**: Attempt to send the request with a generic `object` or common selector (e.g., `img`) and `largeBatch=true` to see if it clears all image-related issues.\n- **Permissions Oracle**: If the `post_id` parameter is strictly validated, iterate through common post IDs. If the `summary` action (which *has* a check) returns a permission error but `edac_insert_ignore_data` returns success for the same ID, authorization bypass is confirmed.","The Equalize Digital Accessibility Checker plugin for WordPress fails to perform authorization checks in its 'edac_insert_ignore_data' AJAX action. This allows authenticated users with Subscriber-level permissions or higher to dismiss or ignore accessibility issues for any post, and even perform mass dismissals across the entire site using the 'largeBatch' parameter.","\u002F\u002F admin\u002Fclass-ajax.php - approx line 819 (reconstructed from patch context in add_ignore method)\n\n\t\tglobal $wpdb;\n\t\t$table_name           = $wpdb->prefix . 'accessibility_checker';\n\t\t$raw_ids              = isset( $_REQUEST['ids'] ) ? (array) wp_unslash( $_REQUEST['ids'] ) : [];\n\t\t$ids                  = array_map(\n\t\t\tfunction ( $value ) {\n\t\t\t\treturn (int) $value;\n\t\t\t},\n\t\t\t$raw_ids\n\t\t);\n\t\t$action               = isset( $_REQUEST['ignore_action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_action'] ) ) : '';\n\t\t$type                 = isset( $_REQUEST['ignore_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_type'] ) ) : '';\n\t\t$siteid               = get_current_blog_id();\n\n        \u002F\u002F Missing: current_user_can('edit_post', $post_id) check","--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Faccessibility-checker\u002F1.42.0\u002Fadmin\u002Fclass-ajax.php\t2026-03-17 18:08:32.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Faccessibility-checker\u002F1.42.1\u002Fadmin\u002Fclass-ajax.php\t2026-05-20 19:37:20.000000000 +0000\n@@ -819,17 +819,54 @@\n \t\t}\n \n \t\tglobal $wpdb;\n-\t\t$table_name           = $wpdb->prefix . 'accessibility_checker';\n-\t\t$raw_ids              = isset( $_REQUEST['ids'] ) ? (array) wp_unslash( $_REQUEST['ids'] ) : []; \u002F\u002F phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization handled below.\n-\t\t$ids                  = array_map(\n+\t\t$table_name = $wpdb->prefix . 'accessibility_checker';\n+\t\t$raw_ids    = isset( $_REQUEST['ids'] ) ? (array) wp_unslash( $_REQUEST['ids'] ) : []; \u002F\u002F phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization handled below.\n+\t\t$ids        = array_map(\n \t\t\tfunction ( $value ) {\n \t\t\t\treturn (int) $value;\n \t\t\t},\n \t\t\t$raw_ids\n \t\t); \u002F\u002F Sanitizing array elements to integers.\n-\t\t$action               = isset( $_REQUEST['ignore_action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_action'] ) ) : '';\n-\t\t$type                 = isset( $_REQUEST['ignore_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_type'] ) ) : '';\n-\t\t$siteid               = get_current_blog_id();\n+\t\t$action     = isset( $_REQUEST['ignore_action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_action'] ) ) : '';\n+\t\t$type       = isset( $_REQUEST['ignore_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_type'] ) ) : '';\n+\t\t$siteid     = get_current_blog_id();\n+\n+\t\t\u002F\u002F Capability check: verify edit_post on every post affected by this request.\n+\t\t$batch_object = null;\n+\t\t$first_id     = reset( $ids );\n+\t\t$valid_table  = edac_get_valid_table_name( $table_name );\n+\n+\t\tif ( ! $first_id || ! $valid_table ) {\n+\t\t\twp_send_json_error( new \\WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) );\n+\t\t}\n+\n+\t\tif ( isset( $_REQUEST['largeBatch'] ) && 'true' === $_REQUEST['largeBatch'] ) {\n+\t\t\t\u002F\u002F largeBatch updates every row sharing the same object string across the site;\n+\t\t\t\u002F\u002F collect all distinct postids that would be affected and check each one.\n+\t\t\t\u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Permission check requires direct lookup.\n+\t\t\t$batch_object = $wpdb->get_var( $wpdb->prepare( 'SELECT object FROM %i WHERE id = %d', $valid_table, $first_id ) );\n+\t\t\tif ( ! $batch_object ) {\n+\t\t\t\twp_send_json_error( new \\WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) );\n+\t\t\t}\n+\t\t\t\u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Permission check requires direct lookup.\n+\t\t\t$affected_post_ids = $wpdb->get_col( $wpdb->prepare( 'SELECT DISTINCT postid FROM %i WHERE siteid = %d AND object = %s', $valid_table, $siteid, $batch_object ) );\n+\t\t} else {\n+\t\t\t\u002F\u002F Small batch: look up the post for every supplied ID in one query.\n+\t\t\t$id_placeholders   = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );\n+\t\t\t$query_args        = array_merge( [ $valid_table ], $ids );\n+\t\t\t$affected_post_ids = $wpdb->get_col( $wpdb->prepare( \"SELECT DISTINCT postid FROM %i WHERE id IN ({$id_placeholders})\", $query_args ) ); \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder -- Permission check requires direct lookup.\n+\t\t}\n+\n+\t\tif ( empty( $affected_post_ids ) ) {\n+\t\t\twp_send_json_error( new \\WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) );\n+\t\t}\n+\n+\t\tforeach ( $affected_post_ids as $affected_post_id ) {\n+\t\t\tif ( ! current_user_can( 'edit_post', (int) $affected_post_id ) ) {\n+\t\t\t\twp_send_json_error( new \\WP_Error( '-5', __( 'Permission Denied', 'accessibility-checker' ) ) );\n+\t\t\t}\n+\t\t}\n+\n \t\t$ignre                = ( 'enable' === $action ) ? 1 : 0;\n \t\t$ignre_user           = ( 'enable' === $action ) ? get_current_user_id() : null;\n \t\t$ignre_user_info      = ( 'enable' === $action ) ? get_userdata( $ignre_user ) : '';\n@@ -844,14 +881,8 @@\n \t\t\u002F\u002F instead of IDs. It is a much less efficient query than by IDs - but many IDs run\n \t\t\u002F\u002F into request size limits which caused this to not function at all.\n \t\tif ( isset( $_REQUEST['largeBatch'] ) && 'true' === $_REQUEST['largeBatch'] ) {\n-\t\t\t\u002F\u002F Get the 'object' from the first id.\n-\t\t\t$first_id = $ids[0];\n-\t\t\t\u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to get the latest value, not a cached value.\n-\t\t\t$object = $wpdb->get_var( $wpdb->prepare( 'SELECT object FROM %i WHERE id = %d', $table_name, $first_id ) );\n-\n-\t\t\tif ( ! $object ) {\n-\t\t\t\twp_send_json_error( new \\WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) );\n-\t\t\t}\n+\t\t\t\u002F\u002F $batch_object was already resolved during the capability check above.\n+\t\t\t$object = $batch_object;\n \t\t\t\u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe variable used for table name, caching not required for one time operation.\n \t\t\t$wpdb->query( $wpdb->prepare( 'UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_comment = %s, ignre_reason = %s, ignre_global = %d WHERE siteid = %d and object = %s', $table_name, $ignre, $ignre_user, $ignre_date, $ignre_comment, $ignre_reason, $ignore_global, $siteid, $object ) );\n \t\t} else {","To exploit this vulnerability, an attacker first authenticates as a Subscriber and obtains a valid security nonce from the WordPress dashboard (localized as 'ajax-nonce' in the plugin's JavaScript parameters). The attacker then identifies the 'object' identifier for an accessibility issue they wish to dismiss. Finally, the attacker sends a POST request to wp-admin\u002Fadmin-ajax.php with the action 'edac_insert_ignore_data', the obtained nonce, the target issue's ID, and the desired ignore state. By setting the 'largeBatch' parameter to 'true', the attacker can dismiss all instances of that accessibility issue across the entire site.","gemini-3-flash-preview","2026-06-04 16:42:35","2026-06-04 16:43:04",{"type":39,"vulnerable_version":40,"fixed_version":11,"vulnerable_browse":41,"vulnerable_zip":42,"fixed_browse":43,"fixed_zip":44,"all_tags":45},"plugin","1.42.0","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Faccessibility-checker\u002Ftags\u002F1.42.0","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Faccessibility-checker.1.42.0.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Faccessibility-checker\u002Ftags\u002F1.42.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Faccessibility-checker.1.42.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Faccessibility-checker\u002Ftags"]