The Exploit
An unauthenticated attacker can craft a form submission that embeds an arbitrary file path (e.g., /var/www/html/wp-config.php) in the file metadata. When a site administrator or automated process deletes the form entry, Forminator will delete that file without validating that it resides within the upload directory or that the path has not been manipulated via traversal sequences.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
action=forminator_submit_form&form_id=1&forminator-1-name=Test&forminator-1-upload=../../wp-config.php
The attacker observes a 200 response indicating successful form submission. When an admin later deletes that entry—either manually or via plugin-configured auto-deletion—wp_delete_file() executes on the path specified in the hidden metadata, removing /var/www/html/wp-config.php from disk.
What the Patch Did
Before
foreach ( $entry_model->meta_data as $meta_data ) {
$meta_value = $meta_data['value'];
if ( is_array( $meta_value ) && isset( $meta_value['file'] ) ) {
$file_path = is_array( $meta_value['file']['file_path'] ) ? $meta_value['file']['file_path'] : array( $meta_value['file']['file_path'] );
if ( ! empty( $file_path ) ) {
foreach ( $file_path as $key => $path ) {
if ( ! empty( $path ) && file_exists( $path ) ) {
wp_delete_file( $path );
}
}
}
}
}
After
$upload_root = wp_upload_dir();
if ( empty( $upload_root['basedir'] ) ) {
return;
}
$upload_root = $upload_root['basedir'];
foreach ( $entry_model->meta_data as $slug => $meta_data ) {
$meta_value = $meta_data['value'];
$field_type = Forminator_Core::get_field_type( $slug );
if ( in_array( $field_type, array( 'upload', 'signature' ), true )
&& is_array( $meta_value ) && isset( $meta_value['file'] ) ) {
$file_path = is_array( $meta_value['file']['file_path'] ) ? $meta_value['file']['file_path'] : array( $meta_value['file']['file_path'] );
if ( ! empty( $file_path ) ) {
foreach ( $file_path as $key => $path ) {
$path = realpath( $path );
if ( ! $path || ! file_exists( $path ) ) {
continue;
}
$basename = wp_basename( $path );
$sanitized = sanitize_file_name( $basename );
if ( $basename !== $sanitized ) {
continue;
}
$normalized_upload_root = wp_normalize_path( $upload_root );
$normalized_path = wp_normalize_path( $path );
if ( ! empty( $normalized_upload_root ) && 0 !== strpos( $normalized_path, $normalized_upload_root ) ) {
continue;
}
wp_delete_file( $path );
}
}
}
}
The patch adds five security controls. First, it retrieves the canonical upload directory using wp_upload_dir()['basedir'] and normalizes it via wp_normalize_path(). Second, it calls realpath() on every path candidate, collapsing symbolic links and traversal sequences (., ..) to their canonical form. Third, it validates the filename basename with sanitize_file_name(), rejecting entries that contain path separators or other suspicious characters. Fourth, it enforces that the normalized path begins with the normalized upload root using strpos(), ensuring the file cannot exist outside the permitted directory. Finally, the patch restricts the deletion operation to fields explicitly marked as upload or signature types, preventing metadata injection attacks on unexpected field categories.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The vulnerability lives in the entry_delete_upload_files() function in library/model/class-form-entry-model.php. During form submission, user-supplied file paths are stored in the entry's meta_data array without validation. An attacker can inject a path like ../../wp-config.php or /etc/passwd into the file_path key. When the entry is later deleted—a process triggered by administrator action or automated expiry logic—the code passes this untrusted path directly to file_exists() and wp_delete_file(). Neither function performs canonicalization or directory confinement, so the operating system resolves the traversal sequences and deletes whatever file the attacker specified.
Why It Works
The load-bearing line is:
$path = realpath( $path );
This function resolves symbolic links, collapses . and .. sequences, and returns a canonical absolute path. If you remove it, an attacker's submission of ../../../wp-config.php will reach the subsequent checks as a string containing literal ../ sequences. The strpos() check against $normalized_upload_root will then fail—not because the path is outside the directory, but because the literal string ../../wp-config.php does not start with /var/www/html/wp-content/uploads. However, if the operating system later resolves it during the wp_delete_file() call, the file will still be deleted.
The engineer added the basename sanitization (sanitize_file_name()) and field-type gating (in_array( $field_type, ... )) as defence-in-depth: they reduce the attack surface by rejecting payloads with suspicious characters and preventing metadata injection into non-file fields. The wp_normalize_path() call is critical because Windows uses backslashes as path separators, and comparing raw strings without normalization could produce false negatives. Together, these controls enforce that only files within the upload directory, with clean basenames, belonging to legitimate upload fields, are eligible for deletion.
Hardening Checklist
- Use
realpath()before any filesystem operation on user-supplied paths. It neutralizes traversal sequences and symlink attacks in a single call; never rely on string-based validation alone. - Confine file operations to a whitelist directory using
wp_upload_dir()andstrpos()prefix matching. All file deletions, reads, and writes should be validated against the canonical upload path. - Validate field type at the metadata layer, not the request layer. Use
Forminator_Core::get_field_type()or equivalent before processing any file metadata, preventing injection into unintended fields. - Sanitize basenames with
sanitize_file_name()and reject any path that contains directory separators. This stops encoded or null-byte payloads from bypassing string checks. - Normalize paths using
wp_normalize_path()before any comparison operation. It abstracts away OS-specific path syntax and prevents bypasses on Windows systems using backslash separators.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-6463