vciy
vciy-2026-0002 · OS4ED/openSIS-Classic · CWE-639

Incomplete fix for CVE-2026-91107: authorization bypass via caller-supplied staff_id and school_id in openSIS-Classic

Commit 556cd31d8734d7c46eb12553775be40eb6cd16de fixed CVE-2026-91107 in modules/users/Staff.php by restricting $_REQUEST['staff_id'] overrides to administrators (User('PROFILE') == 'admin'). Verification of the fix commit and its sibling modules identified two remaining flaws: non-admin users who reach the else branch in Staff.php when $_REQUEST['staff_id'] is set still overwrite $_SESSION['staff_id'] at line 207, and sibling handlers in SchoolsInfoInc.php and User.php accept caller-supplied school_id and staff_id values without verifying administrator privileges.

Receipt & provenance · vciy-2026-0002
Advisory ID
vciy-2026-0002canonical vciy advisory identifier
Upstream project
OS4ED/openSIS-ClassicPHP
Affected versions
openSIS-Classic <= 9.1 (main branch)verified on upstream default branch
Weakness class
CWE-639Authorization Bypass Through User-Controlled Key
Related CVE records
CVE-2026-91107upstream fix commit inspected
Upstream tracking
OS4ED/openSIS-Classic#483reported 2026-10-21 · Published (upstream issue open)
Credit
Jonathan Corners (@JCorners68), Voxell, Inc. (https://vciy.com)discovered and reported
01

Root cause and sibling analysis.

1. Incomplete fix in modules/users/Staff.php (lines 38-55 and 204-208): Commit 556cd31d gated the first assignment block on User('PROFILE') == 'admin', so a non-admin supplying $_REQUEST['staff_id'] != 'new' enters the else branch (which calls SetUserStaffId(User('STAFF_ID'))). However, lines 204-208 still execute unconditionally whenever $_REQUEST['staff_id'] is set and not equal to 'new', calling SetUserStaffId($_REQUEST['staff_id']) and overwriting $_SESSION['staff_id'] with the attacker-supplied value.

2. Unchecked $_REQUEST['school_id'] in modules/schoolsetup/includes/SchoolsInfoInc.php (lines 32-38): Any authenticated user with access to the school information view can supply $_REQUEST['school_id'] to overwrite $_SESSION['UserSchool'] across the session without checking User('PROFILE') == 'admin'.

3. Unchecked $_REQUEST['staff_id'] in modules/users/User.php (lines 58-63): $_REQUEST['staff_id'] is passed to SetUserStaffId($_REQUEST['staff_id']) without verifying User('PROFILE') == 'admin'.

02

Remediation.

Gate line 204 of modules/users/Staff.php, line 32 of modules/schoolsetup/includes/SchoolsInfoInc.php, and line 58 of modules/users/User.php on User('PROFILE') == 'admin'.

Suggested defensive diff
--- a/modules/users/Staff.php
+++ b/modules/users/Staff.php
@@ -201,7 +201,7 @@ if (clean_param($_REQUEST['modfunc'], PARAM_ALPHAMOD) == 'update') {
         }
     }
 }
-if ($_REQUEST['staff_id'] && $_REQUEST['staff_id'] != 'new') {
+if ($_REQUEST['staff_id'] && $_REQUEST['staff_id'] != 'new' && User('PROFILE') == 'admin') {
     if ($_REQUEST['staff_id'] != $_SESSION['staff_id'])
         unset($_SESSION['staff_selected']);
     SetUserStaffId($_REQUEST['staff_id']);
--- a/modules/schoolsetup/includes/SchoolsInfoInc.php
+++ b/modules/schoolsetup/includes/SchoolsInfoInc.php
@@ -29,7 +29,7 @@ include('../../../RedirectIncludes.php');
 
-if (isset($_REQUEST['school_id']) && $_REQUEST['school_id'] != '')
+if (isset($_REQUEST['school_id']) && $_REQUEST['school_id'] != '' && User('PROFILE') == 'admin')
 {
     $_SESSION['UserSchool'] = $_REQUEST['school_id'];
 }
--- a/modules/users/User.php
+++ b/modules/users/User.php
@@ -55,7 +55,7 @@ if (User('PROFILE') != 'admin') {
 }
 
-if ($_REQUEST['staff_id']) {
+if ($_REQUEST['staff_id'] && User('PROFILE') == 'admin') {
     if ($_REQUEST['staff_id'] != 'new') {
         SetUserStaffId($_REQUEST['staff_id']);
     }