From 7be77dacd53ce43dac777a67ad96d55da664bf18 Mon Sep 17 00:00:00 2001 From: velz Date: Tue, 6 Jan 2026 14:45:11 +0530 Subject: [PATCH] FEAT_VAPT_FILE_RESTRICTION&FORM_SANITIZATION --- app/Config/Filters.php | 6 + app/Controllers/ClientController.php | 1 + app/Controllers/EmployeeController.php | 17 ++- app/Filters/GlobalPostFileUploadGuard.php | 147 ++++++++++++++++++++++ app/Filters/SecurityInputFilter.php | 129 +++++++++++++++++++ app/Views/fedeploy.php | 47 ++++++- public/.htaccess | 21 ++++ 7 files changed, 357 insertions(+), 11 deletions(-) create mode 100644 app/Filters/GlobalPostFileUploadGuard.php create mode 100644 app/Filters/SecurityInputFilter.php diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 2128f042..d552bb46 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -16,6 +16,8 @@ use App\Filters\AuthClientApi; use App\Filters\CommissionApiFilter; use App\Filters\VerifyAppSignature; use App\Filters\Cors; +use App\Filters\SecurityInputFilter; +use App\Filters\GlobalPostFileUploadGuard; use App\Filters\AuthJWT; @@ -43,6 +45,8 @@ class Filters extends BaseConfig 'CommissionApiFilter'=> CommissionApiFilter::class, 'appSignature' => VerifyAppSignature::class, 'Cors' => Cors::class, + 'SecurityInputFilter' => SecurityInputFilter::class, + 'GlobalPostFileUploadGuard' => GlobalPostFileUploadGuard::class, ]; /** @@ -56,6 +60,8 @@ class Filters extends BaseConfig 'before' => [ 'HttpRequestLog' => ['except' => 'cli/*'], 'Cors', + 'SecurityInputFilter', + 'GlobalPostFileUploadGuard' // 'csrf', // 'invalidchars', ], diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index 4853fea1..0b6470bc 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -519,6 +519,7 @@ class ClientController extends AdminController public function updateEmpAndPolicyStatus() { $return = $this->clientPolicyModel->updateStatus(); + print_rr($return); $this->myLogger->logme('error', 'Client Policy Status Update Count: {data}', ['data' => $return['client']]); $this->myLogger->logme('error', 'Employee Policy Status Update Count: {data}', ['data' => $return['emp']]); } diff --git a/app/Controllers/EmployeeController.php b/app/Controllers/EmployeeController.php index 118b5330..2c162f1c 100755 --- a/app/Controllers/EmployeeController.php +++ b/app/Controllers/EmployeeController.php @@ -1492,6 +1492,7 @@ class EmployeeController extends AdminController //new step check in S3 if yes then fetch from S3 bucket $s3_key = 'ecard_'.$get_emp_code_and_client_policy_id['name'].'('.$get_emp_code_and_client_policy_id['emp_code'].')'.'_'.$get_emp_code_and_client_policy_id['tpa_id'].'.pdf'; + $s3_key = $this->sanitizeFilePart($s3_key); // echo $s3_key;die(); $s3 = \Config\Services::getS3Service(); if($s3->exists($s3_key) && $mode != 2) //2 => for bulk generate so skip s3 check and generate PDF @@ -3346,7 +3347,8 @@ class EmployeeController extends AdminController public function initiateWellnessOnboard($client_policy_id) { - $r = Jobs::addJob(['job_name' => 'initiateWellnessOnboardJob','payload' => ['client_policy_id' => $client_policy_id]]); + $r = Jobs::addJob(['job_name' => 'initiateWellnessOnboardJob','payload' => ['client_policy_id' => $client_policy_id]]); + //$this->initiateWellnessOnboardJob(['client_policy_id' => $client_policy_id]); return $this->respond(['status' => true, 'code' => 200, 'message' => 'Process started'], 200); } @@ -3377,7 +3379,7 @@ class EmployeeController extends AdminController // echo '==============================';die(); // $data = '[{"id":12847,"employee_id":"TEST_EMP_001","client_policy_id":null,"tpa_id":null,"uhid":null,"batch_code":null,"status":"active","pre_existing_alignments":null,"age_band":null,"basic_cover_si":"0","date_coverage":"2025-01-01","policy_end_date":"2025-12-31","days":"0","premium":"0","rata_premimum":"0","gst":"0","si_enhancement_date":null,"date_of_exit":null,"reason_for_exit":null,"claim_status":"0","created_by":null,"created_at":null,"updated_by":null,"updated_at":null,"is_active":"1","rand_string":null,"ecard_sent_status":"0","payable_employee":"0","file_id":null,"wellness_onboard":"0","name":"test name","relationship":"SELF","emp_code":"TEST_EMP_001","email_corporate":"test@gmail.com","mobile":"9797976565","dob":"1975-08-09"},{"id":12846,"employee_id":"TEST_EMP_001","client_policy_id":null,"tpa_id":null,"uhid":null,"batch_code":null,"status":"active","pre_existing_alignments":null,"age_band":null,"basic_cover_si":"0","date_coverage":"2025-01-01","policy_end_date":"2025-12-31","days":"0","premium":"0","rata_premimum":"0","gst":"0","si_enhancement_date":null,"date_of_exit":null,"reason_for_exit":null,"claim_status":"0","created_by":null,"created_at":null,"updated_by":null,"updated_at":null,"is_active":"1","rand_string":null,"ecard_sent_status":"0","payable_employee":"0","file_id":null,"wellness_onboard":"0","name":"dependent 1","relationship":"SON","emp_code":"TEST_EMP_001","email_corporate":"dependent1@gmail.com","mobile":"9898989898","dob":"2001-08-09"}]'; // $data = (array)json_decode($data,true); - // print_r($data); + // print_r(count($data)); // echo '==============================';die(); if(is_array($data) && count($data)) { @@ -3406,7 +3408,7 @@ class EmployeeController extends AdminController $familiesPayload[$empCode] = $this->buildFamilyPayload($empCode, $members); } - // print_r($familiesPayload);die(); + // print_rr($familiesPayload);die(); $apiResponse = $this->sendFamiliesToWellnessApi($familiesPayload); // print_r($apiResponse); $updatedData = $this->updateWellnessOnboardResponseToDB($apiResponse); @@ -3439,7 +3441,7 @@ class EmployeeController extends AdminController { // Use the first member as primary reference for policy level data $primary = $members[0]; - + // print_rr($primary);die(); // Map DB fields to your required "policyDetails" structure $policyStartDate = $primary['cp_policy_start_date'] ?? null; // $policyStartDate = '2025-01-01'; @@ -3495,6 +3497,7 @@ class EmployeeController extends AdminController public function sendFamiliesToWellnessApi(array $familiesPayload): array { // CI4 HTTP client + // print_rr($familiesPayload);die(); $client = \Config\Services::curlrequest();//die(); $endpointUrl = getenv('WELLNESS_ONBOARD_ENDPOINT_URL'); // Custom headers @@ -3504,7 +3507,7 @@ class EmployeeController extends AdminController ]; foreach ($familiesPayload as $empCode => &$family) { - + // print_rr($family);die(); try { $response = $client->post($endpointUrl, [ 'headers' => $headers, @@ -3522,6 +3525,8 @@ class EmployeeController extends AdminController 'rawBody' => $body, 'data' => $decoded, ]; + + } catch (\Throwable $e) { // In case of exception, store error info $family['apiResponse'] = [ @@ -3531,6 +3536,8 @@ class EmployeeController extends AdminController 'error' => $e->getMessage(), ]; } + + // print_rr($body );die(); } unset($family); // break reference diff --git a/app/Filters/GlobalPostFileUploadGuard.php b/app/Filters/GlobalPostFileUploadGuard.php new file mode 100644 index 00000000..4cc82ba8 --- /dev/null +++ b/app/Filters/GlobalPostFileUploadGuard.php @@ -0,0 +1,147 @@ + ['jpg', 'jpeg'], + 'image/png' => ['png'], + 'image/gif' => ['gif'], + 'image/webp' => ['webp'], + 'image/svg+xml' => ['svg'], + 'application/pdf' => ['pdf'], + 'application/msword' => ['doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['docx'], + 'application/vnd.oasis.opendocument.text' => ['odt'], + 'text/rtf' => ['rtf'], + 'application/rtf' => ['rtf'], + 'application/vnd.ms-excel' => ['xls'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['xlsx'], + 'application/vnd.oasis.opendocument.spreadsheet' => ['ods'], + 'text/csv' => ['csv'], + 'application/csv' => ['csv'], + 'text/plain' => ['txt', 'csv'], + ]; + + protected array $blockedExtensions = [ + 'php', 'phtml', 'pht', 'phar', 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', + 'cgi', 'fcgi', 'pl', 'py', 'rb', 'lua', 'tcl', 'go', 'rs', 'jar', 'class', + 'exe', 'dll', 'com', 'bat', 'cmd', 'msi', 'vbs', 'ps1', 'scr', + 'sh', 'bash', 'zsh', 'apk', 'app', 'deb', 'rpm', 'bin', 'run', + 'js', 'mjs', 'jsp', 'asp', 'aspx', 'cer', 'swf', + 'env', 'ini', 'user.ini', 'htaccess', 'htpasswd', 'conf', 'config', 'log', 'sql', + 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso', + 'lnk', 'url', 'reg', 'sys', 'drv', 'vxd', 'tmp', 'bak', 'old', 'backup', 'key', 'pem' + ]; + + public function before(RequestInterface $request, $arguments = null) + { + if ($request->getMethod() !== 'post') { + return; + } + + $files = $request->getFiles(); + if (empty($files)) { + return; + } + + foreach ($files as $inputName => $fileData) { + $this->validateFileInput($fileData, $inputName); + } + } + + private function validateFileInput($fileData, string $inputName): void + { + if (is_array($fileData)) { + foreach ($fileData as $file) { + $this->validateSingleFile($file, $inputName); + } + } else { + $this->validateSingleFile($fileData, $inputName); + } + } + + private function validateSingleFile($file, string $inputName): void + { + $request = Services::request(); + $clientIp = $request->getIPAddress(); + $uri = $request->getUri()->getPath(); + + if (!$file->isValid()) { + if ($file->getError() === UPLOAD_ERR_INI_SIZE || $file->getError() === UPLOAD_ERR_FORM_SIZE) { + $this->block("File exceeds server-side size limit", $clientIp, $uri, $inputName, $file->getClientName(), 'unknown', 'unknown', 0); + } + return; + } + + $originalName = $file->getClientName(); + $extension = strtolower($file->getExtension()); + $mime = $file->getMimeType(); + $size = $file->getSize(); + + // --- 1. Fixed Null Byte & Path Traversal Check --- + if (preg_match('/\0|[\/\\\]/', $originalName)) { + $this->block("Malicious filename characters", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + + // --- 2. Double Extension Attack Check --- + if (preg_match('/\.(php|phtml|phar|exe|sh|bat|cmd|js|jsp|asp|aspx|py|pl)\./i', $originalName)) { + $this->block("Double extension attack", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + + // --- 3. Forbidden Extension --- + if (in_array($extension, $this->blockedExtensions, true)) { + $this->block("Forbidden extension", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + + // --- 4. File Size Limit --- + if ($size > $this->maxFileSize) { + $this->block("File too large", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + + // --- 5. MIME Allow-list Check --- + if (!array_key_exists($mime, $this->allowedMimeMap)) { + $this->block("MIME type not allowed ($mime)", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + + // --- 6. MIME-Extension Consistency --- + if (!in_array($extension, $this->allowedMimeMap[$mime], true)) { + $this->block("MIME-extension mismatch", $clientIp, $uri, $inputName, $originalName, $mime, $extension, $size); + } + } + + private function block(string $reason, string $ip, string $uri, string $field, string $filename, string $mime, string $ext, int $size): void + { + log_message('critical', + '[UPLOAD_BLOCKED] {reason} | IP: {ip} | URI: {uri} | Field: {field} | File: {file} | MIME: {mime} | EXT: {ext} | SIZE: {size}', + ['reason'=>$reason, 'ip'=>$ip, 'uri'=>$uri, 'field'=>$field, 'file'=>$filename, 'mime'=>$mime, 'ext'=>$ext, 'size'=>$size] + ); + + $response = Services::response(); + $response->setStatusCode(403) + ->setJSON([ + 'status' => 'error', + 'message' => 'File upload rejected: Security policy violation.', + 'debug' => (ENVIRONMENT === 'development') ? $reason : null + ]) + ->send(); + exit; + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) {} +} \ No newline at end of file diff --git a/app/Filters/SecurityInputFilter.php b/app/Filters/SecurityInputFilter.php new file mode 100644 index 00000000..9ee24857 --- /dev/null +++ b/app/Filters/SecurityInputFilter.php @@ -0,0 +1,129 @@ +/i', + + // JavaScript execution vectors + '/javascript\s*:/i', + '/vbscript\s*:/i', + '/data\s*:\s*text\/html/i', + + // Inline event handlers (strong signal) + '/on\w+\s*=\s*["\']?/i', + + // Dangerous HTML tags + '/<\s*iframe\b/i', + '/<\s*object\b/i', + '/<\s*embed\b/i', + '/<\s*applet\b/i', + + // Image-based execution + '/<\s*img\b[^>]*on\w+/i', + + // SVG-based execution (modern bypass) + '/<\s*svg\b/i', + '/<\s*math\b/i', + + // Meta refresh redirect + '/<\s*meta\b[^>]*http-equiv\s*=\s*["\']?refresh/i', + + // HTML injection via src/href + '/<\s*\w+\b[^>]*(src|href)\s*=\s*["\']?\s*(javascript|data)\s*:/i' + ]; + + + public function before(RequestInterface $request, $arguments = null) + { + $logger = Services::mylogger(); + $response = Services::response(); + + // Collect all user-controlled input + $inputs = array_merge( + $request->getGet(), + $request->getPost() + ); + + if (empty($inputs)) { + return; + } + + foreach ($inputs as $field => $value) { + if (is_array($value)) { + $value = json_encode($value); + } + + // Step 1: Canonicalization (VERY IMPORTANT) + $canonical = $this->canonicalize($value); + + // Step 2: Trim (hygiene) + $canonical = trim($canonical); + + // Step 3: Detection (signal-only) + if ($this->detectXss($canonical)) { + + // 🔐 Log intent, not data + $logger->logme('critical','SECURITY_BLOCKED_REQUEST - '. json_encode([ + 'ip' => $request->getIPAddress(), + 'method' => $request->getMethod(), + 'uri' => current_url(), + 'field' => $field, + 'attack' => 'XSS_PATTERN', + 'length' => strlen($canonical), + 'hash' => hash('sha256', $canonical), + ])); + + // ⛔ Block request + return $response + ->setStatusCode(403) + ->setJSON([ + 'status' => 403, + 'error' => 'Forbidden', + 'message' => 'Malicious input detected' + ]); + } + } + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + // no-op + } + + /** + * Canonicalization prevents encoded bypass + */ + private function canonicalize(string $value): string + { + $value = urldecode($value); + $value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // Remove invisible control characters + return preg_replace('/[\x00-\x1F\x7F]/u', '', $value); + } + + private function detectXss(string $value): bool + { + foreach ($this->xssPatterns as $pattern) { + if (preg_match($pattern, $value)) { + return true; + } + } + return false; + } +} diff --git a/app/Views/fedeploy.php b/app/Views/fedeploy.php index 906bf5f7..eca24795 100644 --- a/app/Views/fedeploy.php +++ b/app/Views/fedeploy.php @@ -9,35 +9,70 @@
- +
- + +
- +
+ diff --git a/public/.htaccess b/public/.htaccess index 28a9d8df..c8b54695 100755 --- a/public/.htaccess +++ b/public/.htaccess @@ -5,6 +5,27 @@ Options -Indexes # Rewrite engine # ---------------------------------------------------------------------- +## ADDED for - block any script execution inside folder of public + + Deny from all + # Disable PHP engine + + php_flag engine off + + + # Disable CGI and other executable handlers + Options -ExecCGI + AddHandler cgi-script .php .pl .py .jsp .asp .sh .cgi + + # Block access to any script-like files entirely + + ForceType text/plain + #Order allow,deny + Deny from all + + + + # Turning on the rewrite engine is necessary for the following rules and features. # FollowSymLinks must be enabled for this to work.