From cd18ce0dbfdbb0017e8110bcac751db23f5bbd56 Mon Sep 17 00:00:00 2001 From: wicky <130177258+wicky-zipstack@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:45:05 +0530 Subject: [PATCH 1/2] fix: remove client-side role checks to prevent privilege escalation via session interception - Profile.jsx: remove Role field display that showed intercepted role from session store --- .../components/settings/contents/Profile.jsx | 22 ++------------- .../components/settings/menutree/MenuTree.jsx | 12 +++----- frontend/src/base/route-component.jsx | 28 ++++++++----------- 3 files changed, 18 insertions(+), 44 deletions(-) diff --git a/frontend/src/base/components/settings/contents/Profile.jsx b/frontend/src/base/components/settings/contents/Profile.jsx index ad47587..fd0a79c 100644 --- a/frontend/src/base/components/settings/contents/Profile.jsx +++ b/frontend/src/base/components/settings/contents/Profile.jsx @@ -7,7 +7,6 @@ import { useNavigate } from "react-router-dom"; import { useAxiosPrivate } from "../../../../service/axios-service"; import { orgStore } from "../../../../store/org-store"; -import { useSessionStore } from "../../../../store/session-store"; import { useNotificationService } from "../../../../service/notification-service"; import "./Profile.css"; @@ -19,7 +18,6 @@ const Profile = () => { const axios = useAxiosPrivate(); const navigate = useNavigate(); const { selectedOrgId } = orgStore(); - const { sessionDetails } = useSessionStore(); const { notify } = useNotificationService(); const csrfToken = Cookies.get("csrftoken"); @@ -54,13 +52,13 @@ const Profile = () => { const { data } = await axios.get( `/api/v1/visitran/${selectedOrgId || "default_org"}/profile` ); - form.setFieldsValue({ ...data, role: sessionDetails.user_role }); + form.setFieldsValue(data); const { first_name, last_name, token } = data; initialRef.current = { first_name, last_name, token }; } catch (error) { notify({ error }); } - }, [selectedOrgId, form, sessionDetails.user_role]); + }, [selectedOrgId, form]); const saveProfile = useCallback( async (values) => { @@ -189,22 +187,6 @@ const Profile = () => { - {/* ---------------------- role ---------------------- */} - - - - {/* -------------------- API token ------------------- */} { const { sessionDetails } = useSessionStore(); const isOrgAdmin = sessionDetails?.is_org_admin; - const userRole = sessionDetails?.user_role; // Build settings children dynamically const settingsChildren = useMemo( @@ -171,8 +170,7 @@ const MenuTree = () => { label: "Settings", children: settingsChildren, }, - userRole === "visitran_super_admin" && - uacChildren.length > 0 && { + uacChildren.length > 0 && { key: "user_access_control", icon: , label: "User Access Control", @@ -185,7 +183,7 @@ const MenuTree = () => { children: notificationsChildren, }, ].filter(Boolean), - [settingsChildren, uacChildren, notificationsChildren, userRole] + [settingsChildren, uacChildren, notificationsChildren] ); const handleClick = useCallback( @@ -201,11 +199,9 @@ const MenuTree = () => { ...(notificationsChildren.some((c) => !c.disabled) ? ["notifications"] : []), - ...(userRole === "visitran_super_admin" && uacChildren.length > 0 - ? ["user_access_control"] - : []), + ...(uacChildren.length > 0 ? ["user_access_control"] : []), ], - [notificationsChildren, uacChildren, userRole] + [notificationsChildren, uacChildren] ); return ( diff --git a/frontend/src/base/route-component.jsx b/frontend/src/base/route-component.jsx index c7e2a1c..a0a1efc 100644 --- a/frontend/src/base/route-component.jsx +++ b/frontend/src/base/route-component.jsx @@ -201,22 +201,18 @@ function RouteComponent() { {UserManagement && ( } /> )} - {sessionDetails?.user_role === "visitran_super_admin" && ( - <> - {Roles && } />} - {Resources && ( - } /> - )} - {Permissions && ( - } /> - )} - {SubscriptionAdminPage && ( - } - /> - )} - + {Roles && } />} + {Resources && ( + } /> + )} + {Permissions && ( + } /> + )} + {SubscriptionAdminPage && ( + } + /> )} {Subscriptions && ( } /> From a5be9052dce77add6074e9e19cf3103ca8432f3a Mon Sep 17 00:00:00 2001 From: wicky <130177258+wicky-zipstack@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:57:06 +0530 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20remove=20visitran=5Fsuper=5Fadmin=20?= =?UTF-8?q?shortcut=20in=20checkPermission=20=E2=80=94=20root=20cause=20of?= =?UTF-8?q?=20privilege=20escalation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checkPermission() function in helpers.js had a shortcut that returned true for all permission checks if user_role === "visitran_super_admin". Since user_role is read from localStorage (editable by user), this allowed any user to bypass all frontend permission checks by modifying their session data. Now always uses server-returned permissions from permissionDetails store. --- frontend/src/common/helpers.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/common/helpers.js b/frontend/src/common/helpers.js index 83a1afe..cf97a53 100644 --- a/frontend/src/common/helpers.js +++ b/frontend/src/common/helpers.js @@ -113,10 +113,7 @@ const checkPermission = (resource, action) => { const sessionDetails = useSessionStore.getState().sessionDetails; // Handle case when session is expired/undefined/empty (e.g., after logout) if (!sessionDetails || Object.keys(sessionDetails).length === 0) return false; - const role = sessionDetails.user_role; - // Validate user_role exists - if (!role) return false; - if (role === "visitran_super_admin") return true; + // Always use server-returned permissions — never trust client-side role return permissions[resource]?.[action] ?? false; };