The Exploit
Unauthenticated attacker. Requires a publicly accessible Forminator form with File Upload field, Save and Continue enabled, and file attachments configured in email notifications.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
action=forminator_submit_form_custom&form_id=1&upload-1[file][file_path]=../../../../etc/passwd
The server processes the path traversal payload in the file_path parameter without validation. An attacker observes the /etc/passwd contents embedded in the email notification body or receives the sensitive file as an attachment when the form submission triggers the "Save and Continue" workflow. The attachment filter bypass allows arbitrary files outside the WordPress upload directory to be included in outbound email notifications.
What the Patch Did
Before
$this->attachment = apply_filters( 'forminator_custom_form_mail_attachment', $attachment, $custom_form, $entry, $this->pdfs );
After
$attachment = $this->filter_attachments( $attachment );
$this->attachment = apply_filters( 'forminator_custom_form_mail_attachment', $attachment, $custom_form, $entry, $this->pdfs );
private function filter_attachments( $attachments ) {
if ( ! empty( $attachments ) ) {
$upload_dir = wp_upload_dir();
if ( ! empty( $upload_dir['basedir'] ) ) {
foreach ( $attachments as $key => $attachment ) {
if ( 0 !== strpos( $attachment, $upload_dir['basedir'] ) ) {
unset( $attachments[ $key ] );
}
}
}
}
return $attachments;
}
The patch adds a whitelist validation control using strpos() to enforce that all attachment file paths must begin with the WordPress upload directory (wp_upload_dir()['basedir']). Any attachment path that does not match this prefix is silently removed before being passed to downstream email processing. This is a path confinement check — the functional equivalent of realpath() normalization followed by a basedir containment assertion.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The attacker-controlled upload-1[file][file_path] request parameter flows through form submission handling into the $attachment array in abstract-class-mail.php without path normalization or validation. The vulnerable code at lines 354, 366–378 passes this array directly to the forminator_custom_form_mail_attachment filter hook, which then uses the unsanitized file paths to attach files to email notifications. The trust boundary crossed is from user input (form parameter) to system action (email attachment), and no checkpoint validates that the resolved path stays within /wp-content/uploads/ or equivalent.
Why It Works
The load-bearing line is:
if ( 0 !== strpos( $attachment, $upload_dir['basedir'] ) ) {
Without this string prefix check, the filter_attachments() method collapses to a no-op — the method exists but returns the unfiltered attachment list unchanged. If you removed this single strpos() validation, path traversal sequences like ../../../../etc/passwd would pass through undetected. The engineer added the surrounding loop and unset logic to make the filter work across multiple attachments in a single submission, and the wp_upload_dir() call ensures the whitelist adapts to each site's upload directory configuration (e.g., custom paths, multisite subdirectories). The strpos() check itself is the security gate; everything else is scaffolding to apply that gate consistently.
Hardening Checklist
- Apply
wp_kses_post()orsanitize_file_name()to all user-supplied file path inputs before using them to construct attachment references. The WordPress sanitizer strips directory traversal sequences. - Always use
wp_upload_dir()and validate against its return value before attaching files to emails or serving them. Never trust user-supplied paths in file I/O operations. - Implement a pre-filter hook that runs before
apply_filters()on attachments, enforcing path confinement. Do not rely on third-party hooks to perform security validation. - Unit test the attachment pipeline with malicious inputs (
../../../etc/passwd,..\\..\\windows\\win.ini, symlink paths) to verify the whitelist blocks them. Add regression tests for each patched CWE. - Use
realpath()to resolve symbolic links and normalize paths, then confirm the result starts with the expected basedir. This catches both traversal and symlink-based escapes.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-5192