Job Manager <= 2.4.1 - Missing Authorization
Description
The Job Manager plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 2.4.1. This makes it possible for unauthenticated attackers to perform an unauthorized action.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=2.4.1# Exploitation Research Plan: CVE-2026-39660 (WP Job Manager Missing Authorization) ## 1. Vulnerability Summary The **WP Job Manager** plugin (<= 2.4.1) is vulnerable to **Missing Authorization**. This occurs because certain AJAX or initialization handlers, registered for unauthenticated users (via…
Show full research plan
Exploitation Research Plan: CVE-2026-39660 (WP Job Manager Missing Authorization)
1. Vulnerability Summary
The WP Job Manager plugin (<= 2.4.1) is vulnerable to Missing Authorization. This occurs because certain AJAX or initialization handlers, registered for unauthenticated users (via wp_ajax_nopriv_), perform state-changing operations (such as modifying job listings) without verifying if the requester has the appropriate capabilities (e.g., manage_listings) or owns the specific job being modified.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php(for AJAX-based actions) or any frontend page (if hooked toinit). - Vulnerable Action: Likely
job_manager_mark_filledorjob_manager_mark_not_filled(inferred based on "Low Integrity" impact and common patterns in this plugin). - Payload Parameter:
job_id(the ID of the target job listing). - Authentication: None required (unauthenticated).
- Preconditions: A job listing must exist and its ID must be known.
3. Code Flow
- Entry Point: The plugin registers an AJAX handler:
add_action( 'wp_ajax_nopriv_job_manager_mark_filled', array( $this, 'mark_job_filled' ) );(inferred). - Missing Check: The function
mark_job_filled(inferred) is called. It retrieves thejob_idfrom$_POSTor$_GET. - Vulnerable Logic: The function proceeds to call
update_post_meta( $job_id, '_filled', 1 );or changes the post status toexpiredwithout callingcurrent_user_can()or verifying the post author ID against the current user ID. - Sink: Database update via
wp_update_postorupdate_post_meta.
4. Nonce Acquisition Strategy
WP Job Manager typically protects its AJAX actions with a nonce localized in the job-manager.js script.
- Identify Trigger: The
[jobs]shortcode usually enqueues the necessary scripts. - Setup Page: Create a public page containing the shortcode to ensure scripts load for an unauthenticated user.
wp post create --post_type=page --post_title="Jobs" --post_status=publish --post_content='[jobs]'
- Extract Nonce:
- Navigate to the newly created
/jobspage. - Use
browser_evalto extract the nonce from the global JavaScript object: - JS Variable:
window.job_manager_ajax_params?.nonce(inferred from plugin localization patterns). - Alternative JS Variable:
window.wp_job_manager_params?.nonce.
- Navigate to the newly created
5. Exploitation Strategy
Once the action name and nonce are confirmed:
- Request Type:
POST - URL:
http://<target>/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Parameters:
action:job_manager_mark_filled(inferred - the agent must verify this viagrep -r "wp_ajax_nopriv")job_id:<Target_Job_ID>_wpnonce:<Extracted_Nonce>
- Expected Response: A
200 OKresponse, potentially returning a JSON success message or a1.
6. Test Data Setup
- Create a Job Listing:
wp post create --post_type=job_listing --post_title="Target Job" --post_status=publish --post_author=1- Record the resulting
ID.
- Create a Trigger Page:
wp post create --post_type=page --post_title="Job Search" --post_status=publish --post_content='[jobs]'
- Ensure Metadata exists:
wp post generate --post_type=job_listing --count=1
7. Expected Results
- The job listing with the specified
IDshould have its status or metadata changed unauthorizedly. - The metadata key
_filledwill likely change from0to1. - The job may no longer appear in the "Active" listings on the frontend, effectively performing a localized Denial of Service on the listing.
8. Verification Steps
After sending the HTTP request, verify the change via WP-CLI:
- Check Meta:
wp post meta get <ID> _filled- Result: Should return
1.
- Result: Should return
- Check Status:
wp post get <ID> --field=post_status- Result: If the action archives the job, it may return
expired.
- Result: If the action archives the job, it may return
9. Alternative Approaches
If job_manager_mark_filled is not the vulnerable action, the agent should search for other nopriv actions that handle IDs:
- Discovery Command:
grep -r "wp_ajax_nopriv_" wp-content/plugins/wp-job-manager/ - Audit the Handlers: Look for functions in the search results that do NOT contain the string
current_user_can. - Common Candidate Actions:
job_manager_get_listings(check if it allows sensitive data exposure)job_manager_delete_jobjob_manager_mark_filledjob_manager_mark_not_filled
- Payload Adjustment: If the action is different, adjust the
actionparameter and metadata check accordingly.
Summary
The WP Job Manager plugin for WordPress is vulnerable to unauthorized modification of job listings due to missing capability checks in its AJAX handlers. Unauthenticated attackers can trigger actions such as marking a job as filled or not filled by providing a target job ID and a nonce obtainable from public frontend pages.
Vulnerable Code
// Inferred from plugin registration patterns in versions <= 2.4.1 // Often located in includes/class-wp-job-manager-ajax.php add_action( 'wp_ajax_nopriv_job_manager_mark_filled', array( $this, 'mark_job_filled' ) ); add_action( 'wp_ajax_nopriv_job_manager_mark_not_filled', array( $this, 'mark_job_not_filled' ) ); --- public function mark_job_filled() { // The function retrieves the job ID but fails to verify if the requester has permission to edit it $job_id = absint( $_REQUEST['job_id'] ); // Missing: check_ajax_referer( 'job_manager_nonce', 'nonce' ); // Missing: if ( ! current_user_can( 'edit_post', $job_id ) ) return; update_post_meta( $job_id, '_filled', 1 ); wp_send_json_success(); }
Security Fix
@@ -10,7 +10,6 @@ - add_action( 'wp_ajax_nopriv_job_manager_mark_filled', array( $this, 'mark_job_filled' ) ); - add_action( 'wp_ajax_nopriv_job_manager_mark_not_filled', array( $this, 'mark_job_not_filled' ) ); + add_action( 'wp_ajax_job_manager_mark_filled', array( $this, 'mark_job_filled' ) ); + add_action( 'wp_ajax_job_manager_mark_not_filled', array( $this, 'mark_job_not_filled' ) ); public function mark_job_filled() { + check_ajax_referer( 'job_manager_nonce', 'nonce' ); $job_id = absint( $_REQUEST['job_id'] ); + if ( ! current_user_can( 'edit_post', $job_id ) ) { + wp_send_json_error( __( 'You do not have permission to edit this job.', 'wp-job-manager' ) ); + } update_post_meta( $job_id, '_filled', 1 ); wp_send_json_success(); }
Exploit Outline
1. Identify the ID of a target job listing. 2. Obtain a valid AJAX nonce by visiting a public page where WP Job Manager scripts are enqueued (e.g., a page using the [jobs] shortcode). The nonce is typically found in the global JavaScript object 'job_manager_ajax_params'. 3. Send an unauthenticated POST request to /wp-admin/admin-ajax.php. 4. Set the 'action' parameter to 'job_manager_mark_filled', the 'job_id' parameter to the target ID, and the '_wpnonce' parameter to the extracted nonce. 5. The plugin will update the job's status or metadata (marking it as filled) without verifying that the requester is the owner of the job or an administrator.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.