CVE-2023-2950: Improper Authorization (CWE-285)
value: Improper Authorization in GitHub repository openemr/openemr prior to 7.0.1.
CWE description: The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.
CVSS v3 base score: 8.1
Published: 2023-05-28T04:15Z
Repository: https://github.com/openemr/openemr
Language: PHP
Files changed: portal/patient/libs/Controller/OnsiteDocumentController.php, portal/patient/scripts/app/onsitedocuments.js
Commit date: 2023-04-12 22:02:09 -0400
Commit message: fix: bug fix (#6354)

* fix: bug fix

* use attr instead
Diff: diff --git a/portal/patient/libs/Controller/OnsiteDocumentController.php b/portal/patient/libs/Controller/OnsiteDocumentController.php
index 81e5c077193..0cceb4db8eb 100644
--- a/portal/patient/libs/Controller/OnsiteDocumentController.php
+++ b/portal/patient/libs/Controller/OnsiteDocumentController.php
@@ -249,15 +249,16 @@ class OnsiteDocumentController extends AppBasePortalController
                 $onsitedocument->Pid = $GLOBALS['bootstrap_pid'];
             } else {
                 $onsitedocument->Pid = $this->SafeGetVal($json, 'pid');
             }
 
-            if (!empty($_SESSION["patient_portal_onsite_two"] ?? null)) {
+            // removing for testing
+            /* if (!empty($_SESSION["patient_portal_onsite_two"] ?? null)) {
                 $decode = $this->SafeGetVal($json, 'fullDocument');
                 $k = (int)$this->SafeGetVal($json, 'csrf_token_form')[0];
                 $json->fullDocument = $this->decode($decode, $k);
-            }
+            } */
 
             $onsitedocument->Facility = $this->SafeGetVal($json, 'facility');
             $onsitedocument->Provider = $this->SafeGetVal($json, 'provider');
             $onsitedocument->Encounter = $this->SafeGetVal($json, 'encounter');
             $onsitedocument->CreateDate = date('Y-m-d H:i:s', strtotime($this->SafeGetVal($json, 'createDate')));
@@ -279,10 +280,16 @@ class OnsiteDocumentController extends AppBasePortalController
             $errors = $onsitedocument->GetValidationErrors();
 
             if (count($errors) > 0) {
                 $this->RenderErrorJSON('Please check the form for errors', $errors);
             } else {
+                $new_data = $onsitedocument->FullDocument;
+                // use a custom diff function to look for changing tags only with html
+                if ($new_data != strip_tags($new_data)) {
+                    $old_data = $json->fullDocument;
+                    $onsitedocument->FullDocument = $this->htmlDiff($old_data, $new_data);
+                }
                 $onsitedocument->Save();
                 $this->RenderJSON($onsitedocument, $this->JSONPCallback(), true, $this->SimpleObjectParams());
             }
         } catch (Exception $ex) {
             $this->RenderExceptionJSON($ex);
@@ -300,10 +307,11 @@ class OnsiteDocumentController extends AppBasePortalController
             if (!$json) {
                 throw new Exception('The request body does not contain valid JSON');
             }
             $pk = $this->GetRouter()->GetUrlParam('id');
             $onsitedocument = $this->Phreezer->Get('OnsiteDocument', $pk);
+            $old_data = $onsitedocument->FullDocument;
 
             // only allow patient to update themselves (part 1)
             if (!empty($GLOBALS['bootstrap_pid'])) {
                 if ($GLOBALS['bootstrap_pid'] != $onsitedocument->Pid) {
                     $error = 'Unauthorized';
@@ -316,15 +324,16 @@ class OnsiteDocumentController extends AppBasePortalController
                 $onsitedocument->Pid = $GLOBALS['bootstrap_pid'];
             } else {
                 $onsitedocument->Pid = $this->SafeGetVal($json, 'pid', $onsitedocument->Pid);
             }
 
-            if (!empty($_SESSION["patient_portal_onsite_two"] ?? null)) {
+            // removing for testing
+            /* if (!empty($_SESSION["patient_portal_onsite_two"] ?? null)) {
                 $decode = $this->SafeGetVal($json, 'fullDocument');
                 $k = (int)$this->SafeGetVal($json, 'csrf_token_form')[0];
                 $json->fullDocument = $this->decode($decode, $k);
-            }
+            } */
 
             $onsitedocument->Facility = $this->SafeGetVal($json, 'facility', $onsitedocument->Facility);
             $onsitedocument->Provider = $this->SafeGetVal($json, 'provider', $onsitedocument->Provider);
             $onsitedocument->Encounter = $this->SafeGetVal($json, 'encounter', $onsitedocument->Encounter);
             $onsitedocument->CreateDate = date('Y-m-d H:i:s', strtotime($this->SafeGetVal($json, 'createDate', $onsitedocument->CreateDate)));
@@ -346,10 +355,15 @@ class OnsiteDocumentController extends AppBasePortalController
             $errors = $onsitedocument->GetValidationErrors();
 
             if (count($errors) > 0) {
                 $this->RenderErrorJSON('Please check the form for errors', $errors);
             } else {
+                // use a custom diff function to look for changing tags only with html
+                $new_data = $onsitedocument->FullDocument;
+                if ($new_data != strip_tags($new_data)) {
+                    $onsitedocument->FullDocument = $this->htmlDiff($old_data, $new_data);
+                }
                 $onsitedocument->Save();
                 $this->RenderJSON($onsitedocument, $this->JSONPCallback(), true, $this->SimpleObjectParams());
             }
         } catch (Exception $ex) {
             $this->RenderExceptionJSON($ex);
@@ -383,15 +397,16 @@ class OnsiteDocumentController extends AppBasePortalController
         } catch (Exception $ex) {
             $this->RenderExceptionJSON($ex);
         }
     }
 
-    /**
+    // removing for testing
+    /*
      * @param $encoded
      * @param $v
      * @return bool|string
-     */
+
     private function decode($encoded, $v): bool|string
     {
         $encoded = base64_decode($encoded);
         $decoded = "";
         for ($i = 0; $i < strlen($encoded); $i++) {
@@ -399,6 +414,47 @@ class OnsiteDocumentController extends AppBasePortalController
             $a = $b ^ $v;
             $decoded .= chr($a);
         }
         return base64_decode(base64_decode($decoded));
     }
+    */
+
+    private function diff($old, $new): array
+    {
+        $matrix = array();
+        $maxlen = 0;
+        foreach ($old as $oindex => $ovalue) {
+            $nkeys = array_keys($new, $ovalue);
+            foreach ($nkeys as $nindex) {
+                $matrix[$oindex][$nindex] = isset($matrix[$oindex - 1][$nindex - 1]) ?
+                    $matrix[$oindex - 1][$nindex - 1] + 1 : 1;
+                if ($matrix[$oindex][$nindex] > $maxlen) {
+                    $maxlen = $matrix[$oindex][$nindex];
+                    $omax = $oindex + 1 - $maxlen;
+                    $nmax = $nindex + 1 - $maxlen;
+                }
+            }
+        }
+        if ($maxlen == 0) {
+            return array(array('d' => $old, 'i' => $new));
+        }
+        return array_merge(
+            $this->diff(array_slice($old, 0, $omax), array_slice($new, 0, $nmax)),
+            array_slice($new, $nmax, $maxlen),
+            $this->diff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen))
+        );
+    }
+
+    private function htmlDiff($old, $new): string
+    {
+        $ret = '';
+        $diff = $this->diff(preg_split("/[\s]+/", $old), preg_split("/[\s]+/", $new));
+        foreach ($diff as $k) {
+            if (is_array($k)) {
+                $ret .= (!empty($k['i']) ? attr(implode(' ', $k['i'])) : '');
+            } else {
+                $ret .= $k . ' ';
+            }
+        }
+        return $ret;
+    }
 }
diff --git a/portal/patient/scripts/app/onsitedocuments.js b/portal/patient/scripts/app/onsitedocuments.js
index 726e08aaf3c..239b7f9b921 100644
--- a/portal/patient/scripts/app/onsitedocuments.js
+++ b/portal/patient/scripts/app/onsitedocuments.js
@@ -846,13 +846,14 @@ var page = {
         if (page.encounterFormName && page.encounterFormId) {
             // lbf templates are saved as iframe tag with src url for fetch content on doc load.
             // no frame content is maintained in onsite document activity but template directives are.
             templateContent = templateContent.replace("id=0", "id=" + page.encounterFormId);
         }
-        if (isPortal) {
+        // removing for testing
+        /* if (isPortal) {
             templateContent = page.encode(templateContent, parseInt(csrfTokenDoclib[0]));
-        }
+        } */
         page.onsiteDocument.save({
             'pid': cpid,
             'facility': page.formOrigin, /* 0 portal, 1 dashboard, 2 patient documents */
             'provider': page.onsiteDocument.get('provider'),
             'encounter': page.onsiteDocument.get('encounter'),
@@ -954,17 +955,18 @@ var page = {
                 app.appendAlert(app.getErrorMessage(response), 'alert-error', 0, 'modelAlert');
                 app.hideProgress('modelLoader');
             }
         });
     },
-    encode: function (content, k) {
+    // removing for testing
+    /* encode: function (content, k) {
         let encoded = "";
         content = btoa(content);
         content = btoa(content);
         for (i = 0; i < content.length; i++) {
             let a = content.charCodeAt(i);
             let b = a ^ k;
             encoded = encoded + String.fromCharCode(b);
         }
         return btoa(encoded);
-    }
+    } */
 };

Vulnerable code:             if (!empty($_SESSION["patient_portal_onsite_two"] ?? null)) {
            }
            if (!empty($_SESSION["patient_portal_onsite_two"] ?? null)) {
            }
    /**
     */
        if (isPortal) {
        }
    encode: function (content, k) {
    }
Fixed code:             // removing for testing
            /* if (!empty($_SESSION["patient_portal_onsite_two"] ?? null)) {
            } */
                $new_data = $onsitedocument->FullDocument;
                // use a custom diff function to look for changing tags only with html
                if ($new_data != strip_tags($new_data)) {
                    $old_data = $json->fullDocument;
                    $onsitedocument->FullDocument = $this->htmlDiff($old_data, $new_data);
                }
            $old_data = $onsitedocument->FullDocument;
            // removing for testing
            /* if (!empty($_SESSION["patient_portal_onsite_two"] ?? null)) {
            } */
                // use a custom diff function to look for changing tags only with html
                $new_data = $onsitedocument->FullDocument;
                if ($new_data != strip_tags($new_data)) {
                    $onsitedocument->FullDocument = $this->htmlDiff($old_data, $new_data);
                }
    // removing for testing
    /*

    */

    private function diff($old, $new): array
    {
        $matrix = array();
        $maxlen = 0;
        foreach ($old as $oindex => $ovalue) {
            $nkeys = array_keys($new, $ovalue);
            foreach ($nkeys as $nindex) {
                $matrix[$oindex][$nindex] = isset($matrix[$oindex - 1][$nindex - 1]) ?
                    $matrix[$oindex - 1][$nindex - 1] + 1 : 1;
                if ($matrix[$oindex][$nindex] > $maxlen) {
                    $maxlen = $matrix[$oindex][$nindex];
                    $omax = $oindex + 1 - $maxlen;
                    $nmax = $nindex + 1 - $maxlen;
                }
            }
        }
        if ($maxlen == 0) {
            return array(array('d' => $old, 'i' => $new));
        }
        return array_merge(
            $this->diff(array_slice($old, 0, $omax), array_slice($new, 0, $nmax)),
            array_slice($new, $nmax, $maxlen),
            $this->diff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen))
        );
    }

    private function htmlDiff($old, $new): string
    {
        $ret = '';
        $diff = $this->diff(preg_split("/[\s]+/", $old), preg_split("/[\s]+/", $new));
        foreach ($diff as $k) {
            if (is_array($k)) {
                $ret .= (!empty($k['i']) ? attr(implode(' ', $k['i'])) : '');
            } else {
                $ret .= $k . ' ';
            }
        }
        return $ret;
    }
        // removing for testing
        /* if (isPortal) {
        } */
    // removing for testing
    /* encode: function (content, k) {
    } */