Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev

This commit is contained in:
VENKATESHWARAN 2026-01-06 15:11:31 +05:30
commit c3525b4c1a
7 changed files with 357 additions and 11 deletions

View File

@ -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',
],

View File

@ -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']]);
}

View File

@ -1543,6 +1543,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
@ -3397,7 +3398,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);
}
@ -3428,7 +3430,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))
{
@ -3457,7 +3459,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);
@ -3490,7 +3492,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';
@ -3546,6 +3548,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
@ -3555,7 +3558,7 @@ class EmployeeController extends AdminController
];
foreach ($familiesPayload as $empCode => &$family) {
// print_rr($family);die();
try {
$response = $client->post($endpointUrl, [
'headers' => $headers,
@ -3573,6 +3576,8 @@ class EmployeeController extends AdminController
'rawBody' => $body,
'data' => $decoded,
];
} catch (\Throwable $e) {
// In case of exception, store error info
$family['apiResponse'] = [
@ -3582,6 +3587,8 @@ class EmployeeController extends AdminController
'error' => $e->getMessage(),
];
}
// print_rr($body );die();
}
unset($family); // break reference

View File

@ -0,0 +1,147 @@
<?php
namespace App\Filters;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Filters\FilterInterface;
use Config\Services;
use finfo;
class GlobalPostFileUploadGuard implements FilterInterface
{
/**
* Max file size (in bytes) 25MB
*/
protected int $maxFileSize = 25 * 1024 * 1024;
/**
* Allowed MIME types mapped to extensions
*/
protected array $allowedMimeMap = [
'image/jpeg' => ['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) {}
}

View File

@ -0,0 +1,129 @@
<?php
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
class SecurityInputFilter implements FilterInterface
{
/**
* High-confidence XSS patterns only
* (low false-positive set)
**/
protected array $xssPatterns = [
// Script execution
'/<\s*script\b/i',
'/<\/\s*script\s*>/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;
}
}

View File

@ -9,35 +9,70 @@
<label for="zip_folder">Zip Folder (inside zip to deploy)</label>
<select name="zip_folder" id="zip_folder">
<option value="web/">web/</option>
<option value="dist/">dist/</option>
<!-- <option value="dist/">dist/</option> -->
</select>
</div>
<div>
<label for="s3_bucket">S3 Bucket</label>
<select name="s3_bucket" id="s3_bucket">
<option value="benefits-app-bucket">benefits-app-bucket</option>
<option value="other-bucket">other-bucket</option>
<option value="uat-benefits-app-bucket">UAT Benefits</option>
<option value="uat-hr-app-bucket">UAT HR</option>
<option value="benefits-app-bucket">Live Benefits</option>
<option value="live-hr-app-bucket">Live HR</option>
</select>
</div>
<div>
<label for="s3_prefix">S3 Prefix</label>
<input type="text" name="s3_prefix" id="s3_prefix" value="hr/">
<input type="text" name="s3_prefix" id="s3_prefix" value="/*">
</div>
<div>
<label for="cf_distribution_id">CloudFront Distribution ID (optional)</label>
<input type="text" name="cf_distribution_id" id="cf_distribution_id" value="E1MKRK4U5MZ3BD">
<!-- <input type="text" name="cf_distribution_id" id="cf_distribution_id" value="E1MKRK4U5MZ3BD"> -->
<select name="cf_distribution_id" id="cf_distribution_id">
<option value="EUBZ8CDSV9KZZ">UAT Benefits</option>
<option value="E9TNPRI9ITM1M">UAT HR</option>
<option value="E1MKRK4U5MZ3BD">Live Benefits</option>
<option value="E3TE01DPKHTD8B">Live HR</option>
</select>
</div>
<div>
<label for="cf_paths">
CloudFront Invalidation Paths (comma or newline separated, e.g. <code>/hr/*,/hr/special/*</code>)
</label>
<input type="text" name="cf_paths" id="cf_paths" value="/hr/*">
<input type="text" name="cf_paths" id="cf_paths" value="/*">
<!-- If you prefer multi-line, use <textarea> instead of <input> -->
</div>
<button type="submit">Deploy</button>
</form>
<script>
document.addEventListener('DOMContentLoaded', function () {
const bucketToDistributionMap = {
'uat-benefits-app-bucket': 'EUBZ8CDSV9KZZ',
'uat-hr-app-bucket': 'E9TNPRI9ITM1M',
'benefits-app-bucket': 'E1MKRK4U5MZ3BD',
'live-hr-app-bucket': 'E3TE01DPKHTD8B'
};
const s3BucketEl = document.getElementById('s3_bucket');
const cfDistributionEl = document.getElementById('cf_distribution_id');
s3BucketEl.addEventListener('change', function () {
const selectedBucket = this.value;
if (bucketToDistributionMap[selectedBucket]) {
cfDistributionEl.value = bucketToDistributionMap[selectedBucket];
} else {
// Optional: reset if no mapping found
cfDistributionEl.value = '';
}
});
// Auto-select on page load (useful for edit forms)
s3BucketEl.dispatchEvent(new Event('change'));
});
</script>

View File

@ -5,6 +5,27 @@ Options -Indexes
# Rewrite engine
# ----------------------------------------------------------------------
## ADDED for - block any script execution inside folder of public
<If "%{REQUEST_URI} =~ m#/(logo|add_image_upload|e_card_imgs|assets|claim_sample_forms|sample_import_excel|writable)/#">
Deny from all
# Disable PHP engine
<IfModule mod_php.c>
php_flag engine off
</IfModule>
# 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
<FilesMatch "\.(php|php5|php7|phtml|pl|py|cgi|asp|aspx|sh|rb)$">
ForceType text/plain
#Order allow,deny
Deny from all
</FilesMatch>
</If>
# Turning on the rewrite engine is necessary for the following rules and features.
# FollowSymLinks must be enabled for this to work.
<IfModule mod_rewrite.c>