Multiple Critical Vulnerabilities Patched in WP Job Portal Plugin

Published 12 August 2025
Table of Contents

WP Job Portal Plugin

SQL Injection

8k
CVSS 9.3

This blog post is about an unauthenticated arbitrary file download and an SQL injection vulnerability in the WP Job Portal plugin. If you're a WP Job Portal plugin user, please update the plugin to version 2.3.3.

The vulnerabilities mentioned here were discovered and reported by Patchstack Alliance community member LVT-tholv2k.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About WP Job Portal plugin

The plugin WP Job Portal, which has over 8,000 active installations, allows site owners to deliver a complete recruitment system without having to code. It helps make the hiring process easier, faster, and more accurate.

Unauthenticated SQL Injection

In versions 2.3.2 and below, the plugin is vulnerable to an SQL injection, which allows any unauthenticated attacker to inject SQL queries into the database. The vulnerability has been patched in version 2.3.3 and is tracked with CVE-2025-48274.

The root cause of the issue lies in the validateFormData function:

function validateFormData(&$data) {
    $category = WPJOBPORTALrequest::getVar('parentid');
    $inquery = ' ';
    if ($category) {
        $inquery .=" WHERE parentid = $category ";
    }
    $canupdate = false;
    if ($data['id'] == '') {
        $result = $this->isCategoryExist($data['cat_title']);
        if ($result == true) {
            return WPJOBPORTAL_ALREADY_EXIST;
        } else {
            $query = "SELECT max(ordering)+1 AS maxordering FROM " . wpjobportal::$_db->prefix . "wj_portal_categories " . $inquery;
            $data['ordering'] = wpjobportaldb::get_var($query);
            if ($data['ordering'] == null)
                $data['ordering'] = 1;
        }
//TRIMMED
    }
    return $canupdate;
}

The function validateFormData is taking the $category value from the user-input and concatenating it with the SQL statement variable $inquery. The variable is then concatenated to $query and executed as an SQL statement, which leads to SQLi.

The function validateFormData is called by another function storeCategory , which again is called by the function savecategory which can be executed as a task by a user.

Unauthenticated Arbitrary File Download

In versions 2.3.2 and below, the plugin is vulnerable to an arbitrary file upload, which allows any unauthenticated attacker to download any file from the server. The vulnerability has been patched in version 2.3.3 and is tracked with CVE-2025-48273.

The root cause of the issue lies in the downloadCustomUploadedFile function:

function downloadCustomUploadedFile($upload_for,$file_name,$entity_id){

    $filename = wpjobportalphplib::wpJP_str_replace(' ', '_', $file_name);
    $maindir = wp_upload_dir();
    $basedir = $maindir['basedir'];
    $datadirectory = wpjobportal::$_config->getConfigurationByConfigName('data_directory');

    $path = $basedir . '/' . $datadirectory. '/data';

//TRIMMED

    $file = $path .'/'.$file_name;
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . wpjobportalphplib::wpJP_basename($file));
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    header('Pragma: public');
    header('Content-Length: ' . filesize($file));
    //ob_clean();
    flush();
    if ( ! function_exists( 'WP_Filesystem' ) ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
    }
    global $wp_filesystem;
    if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base') ) {
        $creds = request_filesystem_credentials( site_url() );
        wp_filesystem( $creds );
    }
    // test
    echo $wp_filesystem->get_contents($file);
    exit();
}

The function is taking the parameters and directly concatenating the $file_name variable to $file and retrieving and outputting the file. Tracing back the downloadCustomUploadedFile function, we come across the downloadcustomfile function.

function downloadcustomfile(){
    $nonce = WPJOBPORTALrequest::getVar('_wpnonce');
    if (! wp_verify_nonce( $nonce, 'wpjobportal_field_nonce') ) {
            die( 'Security check Failed' );
    }

    $upload_for = WPJOBPORTALrequest::getVar('upload_for');// to handle different entities(company, job, resume)
    $entity_id = WPJOBPORTALrequest::getVar('entity_id');// to create path for enitity directory where the file is located
    $file_name = WPJOBPORTALrequest::getVar('file_name');// to access the file and download it

    $result = WPJOBPORTALincluder::getJSModel('customfield')->downloadCustomUploadedFile($upload_for,$file_name,$entity_id);
    $msg = WPJOBPORTALMessages::getMessage($result, 'customfield');
    WPJOBPORTALMessages::setLayoutMessage($msg['message'], $msg['status'],$this->_msgkey);
    $url = esc_url_raw(admin_url("admin.php?page=wpjobportal_customfield&ff=".esc_attr($ff)));
    if ($pagenum)
        $url .= "&pagenum=" . $pagenum;
    wp_redirect($url);
    die();
}

The function downloadcustomfile, which is also another task that can be executed by a user, takes $file_name from the user and passes it to downloadCustomUploadedFile. As mentioned above, the value is directly concatenated and retrieved, meaning that it is possible to access any file with path traversal.

Note: Both actions require a nonce to be executed , which is accessible through a page with the shortcode [wpjobportal_my_resumes].

The patch

In version 2.3.3, the vendor patched the SQL injection by ensuring that $category is numeric and added additional esc_sql() check on the variable.

For arbitrary file upload vulnerability, they patched the issue by adding a filename check with wpJP_clean_file_path function.

Removing all occurrences of ./ and .. essentially prevents an attacker from doing path traversal to access an arbitrary file.

Conclusion

It is necessary to ensure that all the SQL queries are properly used with prepared statements when processing with user-inputs. Whenever it comes to downloading or retrieving files, there should be proper filename validation to ensure there is no path traversal leading to arbitrary file access.

Want to learn more about finding and fixing vulnerabilities?

Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.

Timeline

28 April, 2025We received the vulnerability report and notified the vendor.
13 May, 2025The vendor publishes the patched version to the WP repository.
24 May, 2025We published the vulnerability entries to the DB- SQLi & Arbitrary File Download.
12 August, 2025Security advisory article publicly released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The latest in Security Advisories

Looks like your browser is blocking our support chat widget. Turn off adblockers and reload the page.
crossmenu