CVE-2026-4758

WP Job Portal <= 2.4.9 - Authenticated (Subscriber+) Arbitrary File Deletion via Resume Custom File Field

highImproper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
8.8
CVSS Score
8.8
CVSS Score
high
Severity
2.5.0
Patched in
1d
Time to patch

Description

The WP Job Portal plugin for WordPress is vulnerable to arbitrary file deletion due to insufficient file path validation in the 'WPJOBPORTALcustomfields::removeFileCustom' function in all versions up to, and including, 2.4.9. This makes it possible for authenticated attackers, with Subscriber-level access and above, 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:L/UI:N/S:U/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=2.4.9
PublishedMarch 25, 2026
Last updatedMarch 25, 2026
Affected pluginwp-job-portal

What Changed in the Fix

Changes introduced in v2.5.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps required to demonstrate an arbitrary file deletion vulnerability in the **WP Job Portal** plugin (version 2.4.9). An authenticated user with Subscriber (Jobseeker) privileges can delete arbitrary files on the server by exploiting a path traversal vulnerability i…

Show full research plan

This research plan outlines the steps required to demonstrate an arbitrary file deletion vulnerability in the WP Job Portal plugin (version 2.4.9). An authenticated user with Subscriber (Jobseeker) privileges can delete arbitrary files on the server by exploiting a path traversal vulnerability in the removeFileCustom function.


1. Vulnerability Summary

  • Vulnerability: Authenticated Arbitrary File Deletion (Path Traversal)
  • Vulnerable Function: WPJOBPORTALcustomfields::removeFileCustom
  • Location: includes/classes/customfields.php
  • Condition: The plugin fails to validate or sanitize the file path parameter before passing it to unlink(). By providing a path like ../../../../wp-config.php, an attacker can delete sensitive system files, potentially leading to site takeover or Remote Code Execution (RCE) via reconfiguration.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: wpjobportal_remove_custom_file (inferred action name corresponding to the removeFileCustom method).
  • Vulnerable Parameter: file (or file_path).
  • Authentication: Required (Subscriber/Jobseeker role or higher).
  • Preconditions: A user must be logged in as a Jobseeker. The wpjobportal_jobseeker_controlpanel shortcode must be active on a page to facilitate nonce acquisition.

3. Code Flow

  1. Entry Point: An authenticated user sends a POST request to admin-ajax.php with the action wpjobportal_remove_custom_file.
  2. AJAX Dispatch: WordPress triggers the handler registered in the plugin, which maps to WPJOBPORTALcustomfields::removeFileCustom.
  3. Parameter Extraction: The function retrieves the file path from the $_POST['file'] parameter.
  4. Vulnerable Sink: The function performs a check (likely file_exists()) and then calls unlink($file) on the user-supplied path without stripping directory traversal sequences (../).
  5. Result: The file at the traversed path is deleted from the filesystem.

4. Nonce Acquisition Strategy

The plugin typically enqueues a script that localizes an object containing the AJAX URL and a nonce.

  1. Identify Shortcode: The Jobseeker control panel is rendered via the [wpjobportal_jobseeker_controlpanel].
  2. Setup Page: Create a page containing this shortcode.
  3. Navigate: Access the page as a logged-in Subscriber.
  4. Extract Nonce:
    • The plugin localizes the object wpjobportal_ajax_obj.
    • The nonce key is typically nonce.
    • JS Command: window.wpjobportal_ajax_obj?.nonce

5. Exploitation Strategy

The agent will execute the following steps:

Step 1: Preparation

  1. Create a Subscriber user (Jobseeker).
  2. Create a page with the [wpjobportal_jobseeker_controlpanel] shortcode using wp post create.

Step 2: Nonce Extraction

  1. Navigate to the newly created page as the Jobseeker user using browser_navigate.
  2. Extract the nonce: browser_eval("wpjobportal_ajax_obj.nonce").

Step 3: Trigger File Deletion

  1. Use the http_request tool to send a POST request to /wp-admin/admin-ajax.php.
  2. Payload:
    • action: wpjobportal_remove_custom_file
    • nonce: [Extracted Nonce]
    • file: ../../../../wp-config.php (adjusting depth as necessary to reach the WordPress root from the uploads/data directory).

Request Details:

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body: action=wpjobportal_remove_custom_file&nonce=[NONCE]&file=../../../../wp-config.php

6. Test Data Setup

  • WordPress User:
    • Username: jobseeker_attacker
    • Role: subscriber (Note: Plugin may assign custom roles, but Subscriber generally has Jobseeker capabilities).
  • Content:
    • A page with Title "Jobseeker Panel" and Content [wpjobportal_jobseeker_controlpanel].
  • Target File: Ensure wp-config.php exists and is writable by the web server (standard in local test environments).

7. Expected Results

  • Response: The server should return a success message or an empty response (HTTP 200).
  • Side Effect: The wp-config.php file will be deleted from the WordPress root directory.

8. Verification Steps

  1. File Existence Check: Use ls -la /var/www/html/wp-config.php via the terminal. If the file is missing, the exploit is successful.
  2. Site Status: Attempt to visit the site homepage. It should trigger the WordPress installation screen (as wp-config.php is missing), confirming the deletion.

9. Alternative Approaches

If wpjobportal_remove_custom_file is not the correct action name:

  1. Search includes/classes/customfields.php for the string wp_ajax_ or add_action.
  2. Look for any parameter named file or path within removeFileCustom.
  3. Check if the file path is expected relative to a specific directory (e.g., if the plugin prepends wp-content/uploads/wp-job-portal/, adjust the ../ depth accordingly).
  4. Try alternative parameter names like file_path, path, or filename.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP Job Portal plugin for WordPress is vulnerable to arbitrary file deletion due to a path traversal vulnerability in the removeFileCustom function. Authenticated attackers with Subscriber-level access (Jobseekers) can exploit this to delete sensitive files like wp-config.php by providing malicious directory traversal sequences in resume custom file field parameters.

Vulnerable Code

// includes/classes/customfields.php:1557
    function removeFileCustom($wpjobportal_id,$wpjobportal_key,$wpjobportal_uploadfor){
        $filename = wpjobportalphplib::wpJP_str_replace(' ', '_', $wpjobportal_key);
        $wpjobportal_maindir = wp_upload_dir();
        $basedir = $wpjobportal_maindir['basedir'];
        $wpjobportal_datadirectory = wpjobportal::$_config->getConfigurationByConfigName('data_directory');

        $wpjobportal_path = $basedir . '/' . $wpjobportal_datadirectory. '/data';

        if($wpjobportal_uploadfor == 'company'){
            $wpjobportal_path = $wpjobportal_path . '/employer/comp_'.$wpjobportal_id.'/custom_uploads';
        }elseif($wpjobportal_uploadfor == 'job'){
            $wpjobportal_path = $wpjobportal_path . '/employer/job_'.$wpjobportal_id.'/custom_uploads';
        }elseif($wpjobportal_uploadfor == 'resume'){
            $wpjobportal_path = $wpjobportal_path . '/jobseeker/resume_'.$wpjobportal_id.'/custom_uploads';
        }elseif($wpjobportal_uploadfor == 'profile'){
            $wpjobportal_path = $wpjobportal_path . '/profile/profile_'.$wpjobportal_id.'/custom_uploads';
        }


        $wpjobportal_userpath = $wpjobportal_path .'/'.$filename;
        wp_delete_file($wpjobportal_userpath);
        return ;
    }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-job-portal/2.4.9/includes/classes/customfields.php /home/deploy/wp-safety.org/data/plugin-versions/wp-job-portal/2.5.0/includes/classes/customfields.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-job-portal/2.4.9/includes/classes/customfields.php	2026-03-18 04:33:24.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-job-portal/2.5.0/includes/classes/customfields.php	2026-03-25 04:29:28.000000000 +0000
@@ -1557,25 +1557,45 @@
 
     function removeFileCustom($wpjobportal_id,$wpjobportal_key,$wpjobportal_uploadfor){
         $filename = wpjobportalphplib::wpJP_str_replace(' ', '_', $wpjobportal_key);
-
-        $wpjobportal_maindir = wp_upload_dir();
-        $basedir = $wpjobportal_maindir['basedir'];
-        $wpjobportal_datadirectory = wpjobportal::$_config->getConfigurationByConfigName('data_directory');
-
-        $wpjobportal_path = $basedir . '/' . $wpjobportal_datadirectory. '/data';
-
-        if($wpjobportal_uploadfor == 'company'){
-            $wpjobportal_path = $wpjobportal_path . '/employer/comp_'.$wpjobportal_id.'/custom_uploads';
-        }elseif($wpjobportal_uploadfor == 'job'){
-            $wpjobportal_path = $wpjobportal_path . '/employer/job_'.$wpjobportal_id.'/custom_uploads';
-        }elseif($wpjobportal_uploadfor == 'resume'){
-            $wpjobportal_path = $wpjobportal_path . '/jobseeker/resume_'.$wpjobportal_id.'/custom_uploads';
-        }elseif($wpjobportal_uploadfor == 'profile'){
-            $wpjobportal_path = $wpjobportal_path . '/profile/profile_'.$wpjobportal_id.'/custom_uploads';
-        }
 
+        // remove traversal
+        $filename = wpjobportalphplib::wpJP_clean_file_path($filename);
+
+        // Sanitize filename safely
+        $filename = sanitize_file_name($filename);
+
+        if (empty($filename)) {
+            return false;
+        }
+
+        $filetyperesult = wp_check_filetype($filename);
+        $wpjobportal_image_file_type = wpjobportal::$_config->getConfigurationByConfigName('image_file_type');
+        $document_file_type = wpjobportal::$_config->getConfigurationByConfigName('document_file_type');
+
+        $allowed_file_types  = '';
+        $allowed_file_types .= $document_file_type.','.$wpjobportal_image_file_type;
+
+        if(wpjobportalphplib::wpJP_strstr($allowed_file_types, $filetyperesult['ext'])){
+            $wpjobportal_maindir = wp_upload_dir();
+            $basedir = $wpjobportal_maindir['basedir'];
+            $wpjobportal_datadirectory = wpjobportal::$_config->getConfigurationByConfigName('data_directory');
+
+            $wpjobportal_path = $basedir . '/' . $wpjobportal_datadirectory. '/data';
+
+            if($wpjobportal_uploadfor == 'company'){
+                $wpjobportal_path = $wpjobportal_path . '/employer/comp_'.$wpjobportal_id.'/custom_uploads';
+            }elseif($wpjobportal_uploadfor == 'job'){
+                $wpjobportal_path = $wpjobportal_path . '/employer/job_'.$wpjobportal_id.'/custom_uploads';
+            }elseif($wpjobportal_uploadfor == 'resume'){
+                $wpjobportal_path = $wpjobportal_path . '/jobseeker/resume_'.$wpjobportal_id.'/custom_uploads';
+            }elseif($wpjobportal_uploadfor == 'profile'){
+                $wpjobportal_path = $wpjobportal_path . '/profile/profile_'.$wpjobportal_id.'/custom_uploads';
+            }
 
-        $wpjobportal_userpath = $wpjobportal_path .'/'.$filename;
-        wp_delete_file($wpjobportal_userpath);
+            $wpjobportal_userpath = $wpjobportal_path .'/'.$filename;
+            wp_delete_file($wpjobportal_userpath);
+        }
         return ;
     }

Exploit Outline

To exploit this vulnerability, an attacker must first authenticate as a Subscriber (Jobseeker). They need to obtain a valid nonce for the 'wpjobportal_remove_custom_file' AJAX action, which can typically be found by visiting a page containing the [wpjobportal_jobseeker_controlpanel] shortcode. Once authenticated and in possession of the nonce, the attacker sends a POST request to '/wp-admin/admin-ajax.php' with the action set to 'wpjobportal_remove_custom_file' and a 'file' parameter containing a path traversal payload (e.g., '../../../../wp-config.php'). The plugin processes this request and calls wp_delete_file on the resulting path, deleting the target file from the server.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.