On August 4th, the LedgerSMB project was advised of a security vulnerability in the code. Please see below our security advisory.


  DOM cross-site scripting of authenticated users in LedgerSMB

Summary:
========

  LedgerSMB does not check the origin of HTML fragments merged into the
  browser's DOM.  By sending a specially crafted URL to an authenticated user,
  this flaw can be abused for remote code execution and information disclosure.


Known vulnerable:
=================

  All of:

  - 1.5.0 upto 1.5.30 (including)
  - 1.6.0 upto 1.6.33 (including)
  - 1.7.0 upto 1.7.32 (including)
  - 1.8.0 upto 1.8.17 (including)


Known fixed:
============

  - 1.7.33
  - 1.8.18


Details:
========

  LedgerSMB uses the URL's hash fragment (the part after the '#'-sign) to
  store which screen the user is on, for the purpose of history navigation
  (the back button).  The hash fragment is assumed to be a URL that is part
  of the web application's URL space.  This assumption is not verified.
  This allows an attacker inject a script into the page by send a specially
  crafted URL to an authenticated user.  As the process of loading the attack
  payload overwrites any content in the main window, the attack by itself does
  not expose sensitive information; a sophisticated payload in addition to
  targetting a sufficiently privileged user, is required for information
  disclosure.

  Proper audit control and separation of duties limit Integrity impact of
  the attack vector.

  The vulnerable code dates back to version 1.5 when LedgerSMB moved to the
  Single Page Application (SPA) model for web applications.



Severity:
=========

  CVSSv3.1 Base Score: 7.1 (High)

  CVSSv3.1 Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N


Recommendations:
================

  We recommend all users to upgrade to known-fixed versions. Versions prior
  to 1.7 are end-of-life and will not receive security fixes from the
  LedgerSMB project.

  Users who cannot upgrade, may apply the included patches or are advised
  to contact a vendor for custom support.

  There are no workarounds available for this vulnerability.


References:
===========

  CVE-2021-3693  (LedgerSMB)

  https://ledgersmb.org/cve-2021-3693-cross-site-scripting

  https://huntr.dev/bounties/daf1384d-648a-43fd-9b35-5c37d8ead667/



Reported by:
============

  ranjit-git, user of the huntr.dev platform


Patches:
========


Patch for LedgerSMB 1.8 (1.8.0 upto 1.8.17, including):
-------------------------------------------------------

[[[
diff --git a/UI/js-src/lsmb/MainContentPane.js b/UI/js-src/lsmb/MainContentPane.js
index 9c0d6113c..ebfa36f05 100644
--- a/UI/js-src/lsmb/MainContentPane.js
+++ b/UI/js-src/lsmb/MainContentPane.js
@@ -7,8 +7,8 @@ define([
    "dojo/dom-style",
    "dojo/_base/lang",
    "dojo/promise/Promise",
+   "dojo/Deferred",
    "dojo/promise/all",
-   "dojo/request/xhr",
    "dojo/query",
    //   "dojo/request/iframe",
    "dojo/dom-class"
@@ -19,12 +19,21 @@ define([
    domStyle,
    lang,
    Promise,
+   Deferred,
    all,
-   xhr,
    query,
    //   iframe,
    domClass
 ) {
+   var docURL = new URL(document.location);
+   var domReject = function (request) {
+       return (
+           request.getResponseHeader("X-LedgerSMB-App-Content") !== "yes" ||
+           (request.getResponseHeader("Content-Disposition") || "").startsWith(
+              "attachment"
+           )
+       );
+   };
    return declare("lsmb/MainContentPane", [ContentPane], {
       last_page: null,
       interceptClick: null,
@@ -74,17 +83,62 @@ define([
          );
       },
       load_form: function (url, options) {
+         var tgt = new URL(url, docURL);
+         if (tgt.origin !== docURL.origin) {
+            return new Deferred().resolve();
+         }
+
          var self = this;
          self.fade_main_div();
-         return xhr(url, options).then(
-            function (doc) {
-               self.hide_main_div();
-               self.set_main_div(doc);
-            },
-            function (err) {
-               self.show_main_div();
-               self.report_request_error(err);
-            }
+         var req = new XMLHttpRequest();
+         var dfd = new Deferred(function () {
+            req.abort();
+         });
+         try {
+             req.open(options.method || "GET", tgt);
+             var headers = options.headers;
+             for (var hdr in headers || {}) {
+                 req.setRequestHeader(hdr, headers[hdr]);
+             }
+             if (
+                 options.data &&
+                     !(options.data instanceof FormData) &&
+                     !headers["Content-Type"]
+             ) {
+                 req.setRequestHeader(
+                     "Content-Type",
+                     "application/x-www-form-urlencoded"
+                 );
+             }
+             req.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+             req.addEventListener("load", function () {
+                 dfd.resolve(req);
+             });
+             req.addEventListener("error", function () {
+                 dfd.reject(req);
+             });
+             req.send(options.data || "");
+         } catch (e) {
+             dfd.reject(e);
+         }
+
+         return dfd.then(
+             function (request) {
+                 if (domReject(request)) {
+                     return self.show_main_div();
+                 }
+
+                 self.hide_main_div();
+                 return self.set_main_div(request.response);
+             },
+             function (request) {
+                 if (domReject(request)) {
+                     return self.show_main_div();
+                 }
+
+                 self.show_main_div();
+                 return self.report_request_error({ err: request });
+             }
          );
       },
       download_link: function (/*href*/) {
diff --git a/lib/LedgerSMB/Middleware/DynamicLoadWorkflow.pm b/lib/LedgerSMB/Middleware/DynamicLoadWorkflow.pm
index 99a964548..07f2feb34 100644
--- a/lib/LedgerSMB/Middleware/DynamicLoadWorkflow.pm
+++ b/lib/LedgerSMB/Middleware/DynamicLoadWorkflow.pm
@@ -29,6 +29,7 @@ use HTTP::Status qw/ HTTP_REQUEST_ENTITY_TOO_LARGE /;
 use List::Util qw{ none any };
 use Module::Runtime qw/ use_module /;
 use Plack::Request;
+use Plack::Util;
 use Plack::Util::Accessor qw( script script_name module );
 
 use LedgerSMB::PSGI::Util;
@@ -91,7 +92,15 @@ sub call {
     $env->{'lsmb.script_name'} = $self->script_name;
     $env->{'lsmb.action'} = $action;
     $env->{'lsmb.action_name'} = $action_name;
-    return $self->app->($env);
+    return Plack::Util::response_cb(
+        $self->app->($env),
+        sub {
+            if (not Plack::Util::header_exists($_[0]->[1],
+                                               'X-LedgerSMB-App-Content')) {
+                Plack::Util::header_push($_[0]->[1],
+                                         'X-LedgerSMB-App-Content', 'yes');
+            }
+        });
 }
 
 
diff --git a/lib/LedgerSMB/PSGI.pm b/lib/LedgerSMB/PSGI.pm
index 26b4ec6b3..b0b20a95b 100644
--- a/lib/LedgerSMB/PSGI.pm
+++ b/lib/LedgerSMB/PSGI.pm
@@ -119,7 +119,15 @@ sub old_app {
         my $env = shift;
         local $psgi_env = $env;
 
-        return $handler->($env);
+        return Plack::Util::response_cb(
+            $handler->($env),
+            sub {
+                if (not Plack::Util::header_exists($_[0]->[1],
+                                                   'X-LedgerSMB-App-Content')) {
+                    Plack::Util::header_push($_[0]->[1],
+                                             'X-LedgerSMB-App-Content', 'yes');
+                }
+            });
     }
 }
 
]]]

Patch for LedgerSMB 1.7 (1.7.0 upto 1.7.32, including):
-------------------------------------------------------

[[[
diff --git a/UI/js-src/lsmb/MainContentPane.js b/UI/js-src/lsmb/MainContentPane.js
index 17ac8d391..5bc1bd4fa 100644
--- a/UI/js-src/lsmb/MainContentPane.js
+++ b/UI/js-src/lsmb/MainContentPane.js
@@ -6,6 +6,7 @@ define([
     "dojo/dom-style",
     "dojo/_base/lang",
     "dojo/promise/Promise",
+    "dojo/Deferred",
     "dojo/on",
     "dojo/hash",
     "dojo/promise/all",
@@ -15,8 +16,14 @@ define([
     "dojo/dom-class"
     ],
        function(ContentPane, declare, event, registry, style,
-                lang, Promise, on, hash, all, xhr, query, iframe,
+                lang, Promise, Deferred, on, hash, all, xhr, query, iframe,
                 domClass) {
+           var docURL = new URL(document.location);
+           var domReject = function (request) {
+               return (
+                   request.getResponseHeader("X-LedgerSMB-App-Content") !== "yes" ||
+                   (request.getResponseHeader("Content-Disposition") || "").startsWith("attachment"));
+           };
            return declare("lsmb/MainContentPane",
                           [ContentPane],
               {
@@ -56,17 +63,61 @@ define([
                               });
                   },
                   load_form: function(url, options) {
+                      var tgt = new URL(url, docURL);
+                      if (tgt.origin !== docURL.origin) {
+                          return (new Deferred()).resolve();
+                      }
+
                       var self = this;
                       self.fade_main_div();
-                      return xhr(url, options).then(
-                          function(doc){
+                      var req = new XMLHttpRequest();
+                      var dfd = new Deferred(function () {
+                          req.abort();
+                      });
+                      try {
+                          req.open(options.method || "GET", tgt);
+                          var headers = options.headers || {};
+                          for (var hdr in headers) {
+                              req.setRequestHeader(hdr, headers[hdr]);
+                          }
+                          if (options.data &&
+                              !(options.data instanceof FormData) &&
+                              ! headers["Content-Type"]) {
+                              req.setRequestHeader(
+                                  "Content-Type",
+                                  "application/x-www-form-urlencoded"
+                              );
+                          }
+                          req.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+                          req.addEventListener("load", function () {
+                              dfd.resolve(req);
+                          });
+                          req.addEventListener("error", function () {
+                              dfd.reject(req);
+                          });
+                          req.send(options.data || "");
+                      } catch (e) {
+                          dfd.reject(e);
+                      }
+
+                      return dfd.then(
+                          function (request) {
+                              if (domReject(request)) {
+                                  return self.show_main_div();
+                              }
+
                               self.hide_main_div();
-                              self.set_main_div(doc);
+                              return self.set_main_div(request.response);
                           },
-                          function(err){
+                          function (request) {
+                              if (domReject(request)) {
+                                  return self.show_main_div();
+                              }
+
                               self.show_main_div();
-                              self.report_request_error(err);
-                          });
+                              return self.report_request_error({ err: request });
+                          }
+                      );
                   },
                   download_link: function(href) {
                       // while it would have been nice for the code below
diff --git a/lib/LedgerSMB/Middleware/DynamicLoadWorkflow.pm b/lib/LedgerSMB/Middleware/DynamicLoadWorkflow.pm
index 94737a25d..6964798a5 100644
--- a/lib/LedgerSMB/Middleware/DynamicLoadWorkflow.pm
+++ b/lib/LedgerSMB/Middleware/DynamicLoadWorkflow.pm
@@ -28,6 +28,7 @@ use parent qw ( Plack::Middleware );
 use List::Util qw{ none any };
 use Module::Runtime qw/ use_module /;
 use Plack::Request;
+use Plack::Util;
 
 use LedgerSMB::PSGI::Util;
 
@@ -95,7 +96,15 @@ sub call {
     $env->{'lsmb.script_name'} = $script_name;
     $env->{'lsmb.action'} = $action;
     $env->{'lsmb.action_name'} = $action_name;
-    return $self->app->($env);
+    return Plack::Util::response_cb(
+        $self->app->($env),
+        sub {
+            if (not Plack::Util::header_exists($_[0]->[1],
+                                               'X-LedgerSMB-App-Content')) {
+                Plack::Util::header_push($_[0]->[1],
+                                         'X-LedgerSMB-App-Content', 'yes');
+            }
+        });
 }
 
 
diff --git a/lib/LedgerSMB/PSGI.pm b/lib/LedgerSMB/PSGI.pm
index 328e80ad3..ec8be07f1 100644
--- a/lib/LedgerSMB/PSGI.pm
+++ b/lib/LedgerSMB/PSGI.pm
@@ -78,7 +78,7 @@ Returns a 'PSGI app' which handles requests for the 'old-code' scripts in old/bi
 =cut
 
 sub old_app {
-    return CGI::Emulate::PSGI->handler(
+    my $handler = CGI::Emulate::PSGI->handler(
         sub {
             my $uri = $ENV{REQUEST_URI};
             $uri =~ s/\?.*//;
@@ -86,6 +86,18 @@ sub old_app {
 
             _run_old();
         });
+
+    return sub {
+        return Plack::Util::response_cb(
+            $handler->(@_),
+            sub {
+                if (not Plack::Util::header_exists($_[0]->[1],
+                                                   'X-LedgerSMB-App-Content')) {
+                    Plack::Util::header_push($_[0]->[1],
+                                             'X-LedgerSMB-App-Content', 'yes');
+                }
+            });
+    }
 }
 
 
]]]


--
Bye,

Erik.

http://efficito.com -- Hosted accounting and ERP.
Robust and Flexible. No vendor lock-in.