From 0020d0676cd9dad88192193206ff719feea32c6e Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Thu, 22 Oct 2026 15:19:28 +0530 Subject: [PATCH] feat(security): restrict /app/queue and live OPD token feed to authorized hospital IPs (WASA CWE-200 remediation) --- app/Config/Routes.php | 1 + app/Controllers/Api/v1/DoctorApi.php | 240 ++++++++++++++++++ app/Controllers/Setting/OpdQueueSetting.php | 25 +- app/Views/Setting/Admin/opd_queue_setting.php | 107 ++++++++ 4 files changed, 372 insertions(+), 1 deletion(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d0b45da..8096a5e 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1467,6 +1467,7 @@ $routes->group('setting', static function($routes) { $routes->post('admin/opd-queue-tv/save', 'Setting\\OpdQueueSetting::save'); $routes->post('admin/opd-queue-tv/upload-banner', 'Setting\\OpdQueueSetting::uploadBanner'); $routes->post('admin/opd-queue-tv/delete-banner', 'Setting\\OpdQueueSetting::deleteBanner'); + $routes->post('admin/opd-queue-tv/save-ip-security', 'Setting\\OpdQueueSetting::saveIpSecurity'); $routes->get('admin/medical-store', 'Setting\\MedicalStoreAdmin::index'); $routes->post('admin/medical-store/save', 'Setting\\MedicalStoreAdmin::save'); $routes->post('admin/medical-store/delete', 'Setting\\MedicalStoreAdmin::delete'); diff --git a/app/Controllers/Api/v1/DoctorApi.php b/app/Controllers/Api/v1/DoctorApi.php index 7030c0a..5f1ee26 100644 --- a/app/Controllers/Api/v1/DoctorApi.php +++ b/app/Controllers/Api/v1/DoctorApi.php @@ -872,6 +872,12 @@ class DoctorApi extends BaseController */ public function queueIndex() { + if (! $this->isQueueAccessAllowed()) { + return $this->response + ->setStatusCode(403) + ->setBody($this->renderQueueAccessDeniedHtml()); + } + $pwaPath = FCPATH . 'App/Queue/index.html'; if (file_exists($pwaPath)) { return $this->response->setBody(file_get_contents($pwaPath)); @@ -907,12 +913,246 @@ class DoctorApi extends BaseController return $this->response->setStatusCode(404); } + /** + * Verifies if the incoming request is authorized to access the OPD TV Queue Display / API. + * Access is allowed if: + * 1. User has an active authenticated HMS session (CodeIgniter Shield / user_id) + * 2. TV_QUEUE_IP_RESTRICTION_ENABLED setting is set to '0' (disabled) + * 3. Request origin matches allowed IPs/subnets (TV_QUEUE_ALLOWED_IPS) + */ + private function isQueueAccessAllowed(): bool + { + // 1. Logged in staff / administrator can always view + if ((function_exists('auth') && auth()->loggedIn()) || (session()->has('user_id') && (int) session()->get('user_id') > 0)) { + return true; + } + + $db = db_connect(); + $enabledRow = $db->table('hospital_setting') + ->select('s_value') + ->where('s_name', 'TV_QUEUE_IP_RESTRICTION_ENABLED') + ->get(1) + ->getRowArray(); + + $restrictionEnabled = ($enabledRow['s_value'] ?? '1') === '1'; + if (! $restrictionEnabled) { + return true; + } + + // 2. Resolve client IP + $clientIp = trim((string) $this->request->getIPAddress()); + if ($clientIp === '' || $clientIp === '0.0.0.0') { + $clientIp = trim((string) ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1')); + } + + // 3. Fetch allowed IP rules + $allowedRow = $db->table('hospital_setting') + ->select('s_value') + ->where('s_name', 'TV_QUEUE_ALLOWED_IPS') + ->get(1) + ->getRowArray(); + + $defaultAllowed = "127.0.0.1\n::1\n10.*\n192.168.*\n172.16.*\n103.251.140.182"; + $allowedIpsRaw = !empty($allowedRow['s_value']) ? (string) $allowedRow['s_value'] : $defaultAllowed; + + $rules = preg_split('/[\r\n,]+/', $allowedIpsRaw, -1, PREG_SPLIT_NO_EMPTY); + foreach ($rules as $rule) { + $rule = trim($rule); + if ($rule === '') continue; + + if ($this->matchIpRule($clientIp, $rule)) { + return true; + } + } + + return false; + } + + /** + * Checks if a client IP matches an individual IP rule (exact, wildcard, or CIDR). + */ + private function matchIpRule(string $clientIp, string $rule): bool + { + if ($clientIp === $rule) { + return true; + } + + if ($rule === 'localhost' && in_array($clientIp, ['127.0.0.1', '::1'], true)) { + return true; + } + + if (in_array(strtolower($rule), ['intranet', 'private', 'lan'], true)) { + return $this->isHospitalIntranet($clientIp); + } + + // Check wildcard (e.g. 192.168.*, 10.*, 103.251.*) + if (strpos($rule, '*') !== false) { + if (fnmatch($rule, $clientIp)) { + return true; + } + } + + // Check CIDR (e.g. 192.168.0.0/16 or 10.0.0.0/8) + if (strpos($rule, '/') !== false) { + [$subnet, $bits] = explode('/', $rule, 2); + $bits = (int) $bits; + $ipLong = ip2long($clientIp); + $subnetLong = ip2long(trim($subnet)); + if ($ipLong !== false && $subnetLong !== false && $bits >= 0 && $bits <= 32) { + $mask = $bits === 0 ? 0 : (~0 << (32 - $bits)); + if (($ipLong & $mask) === ($subnetLong & $mask)) { + return true; + } + } + } + + return false; + } + + /** + * Renders a branded 403 Forbidden screen when /app/queue is accessed externally. + */ + private function renderQueueAccessDeniedHtml(): string + { + $clientIp = esc((string) $this->request->getIPAddress()); + $loginUrl = base_url('login'); + + return << + + + + + 403 Forbidden • OPD Queue TV Display + + + + + +
+
+ Restricted Hospital Terminal (CWE-200) +
+
+ +
+

OPD TV Display Access Restricted

+

+ This live queue & token tracker display is restricted to authorized hospital waiting rooms, local intranet (LAN), and secure VPN terminals. +

+ +
+ Client IP: {$clientIp} • Status: Unauthorized +
+ +

+ If you are hospital staff setting up a waiting room display, please add this IP to the whitelist in Admin Panel > OPD Queue TV Settings or log in using your staff credentials. +

+ + +
+ + +HTML; + } + /** * GET api/v1/opd/queue/live * Returns real-time OPD Tokens & Hospital Showcase Ads for TV Queue Display */ public function liveQueueDisplay() { + if (! $this->isQueueAccessAllowed()) { + return $this->response + ->setStatusCode(403) + ->setJSON([ + 'status' => 0, + 'error' => 'Forbidden', + 'message' => 'Access Restricted: OPD Live Queue feed is only accessible from authorized hospital terminals and networks (WASA CWE-200 Remediation).', + 'client_ip' => (string) $this->request->getIPAddress(), + ]); + } + $db = db_connect(); $todayStart = date('Y-m-d') . ' 00:00:00'; $tomorrowStart = date('Y-m-d', strtotime('+1 day')) . ' 00:00:00'; diff --git a/app/Controllers/Setting/OpdQueueSetting.php b/app/Controllers/Setting/OpdQueueSetting.php index e7d3da8..12e08d0 100644 --- a/app/Controllers/Setting/OpdQueueSetting.php +++ b/app/Controllers/Setting/OpdQueueSetting.php @@ -28,12 +28,20 @@ class OpdQueueSetting extends BaseController $customAds = json_decode($adsJson, true) ?: []; } + $ipRestrictionEnabled = $this->getSetting('TV_QUEUE_IP_RESTRICTION_ENABLED', '1'); + $defaultAllowedIps = "127.0.0.1\n::1\n10.*\n192.168.*\n172.16.*\n103.251.140.182"; + $allowedIps = $this->getSetting('TV_QUEUE_ALLOWED_IPS', $defaultAllowedIps); + $currentClientIp = (string) $this->request->getIPAddress(); + return view('Setting/Admin/opd_queue_setting', [ 'footer_ticker' => $tickerText, 'left_ad_text' => $leftAdText, 'left_ad_image' => $leftAdImage, 'left_ad_enabled' => $leftAdEnabled, - 'custom_ads' => $customAds + 'custom_ads' => $customAds, + 'ip_restriction_enabled' => $ipRestrictionEnabled, + 'allowed_ips' => $allowedIps, + 'current_client_ip' => $currentClientIp, ]); } @@ -148,6 +156,21 @@ class OpdQueueSetting extends BaseController ]); } + public function saveIpSecurity() + { + $post = $this->request->getPost(); + $enabled = isset($post['ip_restriction_enabled']) && $post['ip_restriction_enabled'] === '1' ? '1' : '0'; + $allowedIps = trim((string) ($post['allowed_ips'] ?? '')); + + $this->setSetting('TV_QUEUE_IP_RESTRICTION_ENABLED', $enabled); + $this->setSetting('TV_QUEUE_ALLOWED_IPS', $allowedIps); + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'OPD Queue TV IP restriction settings updated successfully!' + ]); + } + private function getSetting(string $name, string $default = ''): string { $row = $this->db->table('hospital_setting') diff --git a/app/Views/Setting/Admin/opd_queue_setting.php b/app/Views/Setting/Admin/opd_queue_setting.php index 21eead5..c964d5d 100644 --- a/app/Views/Setting/Admin/opd_queue_setting.php +++ b/app/Views/Setting/Admin/opd_queue_setting.php @@ -57,6 +57,64 @@ + + +
+
+ TV Display Network & IP Security + WASA Security +
+
+
+
+ +
+ Protection for /app/queue & live token feeds: Only whitelisted hospital local IPs (LAN), VPN addresses, or authorized static IPs can open the TV Display. All unauthorized public internet requests receive HTTP 403 Forbidden with zero doctor/queue data exposed. +
+
+ +
+ > + + When enabled, only allowed IPs or logged-in staff can access /app/queue and live OPD data. +
+ +
+
+ + +
+ +
+ Enter one IP or subnet per line or separated by commas.
+ • Exact: 103.251.140.182, 127.0.0.1
+ • Wildcards: 192.168.*, 10.*, 172.16.*
+ • Subnet CIDR: 10.8.0.0/24, 192.168.1.0/24 +
+
+ +
+ + + +
+ + +
+
+
@@ -232,4 +290,53 @@ } }); } + + var myCurrentIp = ''; + + function addCurrentClientIp() { + if (!myCurrentIp) return; + insertIpRule(myCurrentIp); + } + + function insertIpRule(rule) { + var el = document.getElementById('allowed_ips_textarea'); + if (!el) return; + var cur = el.value.trim(); + var rules = rule.split('\n'); + rules.forEach(function(r) { + r = r.trim(); + if (r !== '' && cur.indexOf(r) === -1) { + cur = cur === '' ? r : cur + '\n' + r; + } + }); + el.value = cur; + } + + function saveIpSecuritySettings(e) { + if (e && e.preventDefault) e.preventDefault(); + var form = document.getElementById('form_ip_security_settings'); + if (!form) return false; + var formData = new FormData(form); + + $.ajax({ + url: '', + type: 'POST', + data: formData, + contentType: false, + processData: false, + dataType: 'json', + success: function(res) { + if (res.status === 1) { + alert(res.message); + refreshAdminSubPage(); + } else { + alert(res.message || 'Error saving IP security settings'); + } + }, + error: function() { + alert('Error saving IP security settings'); + } + }); + return false; + }