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.
Robust and Flexible. No vendor lock-in.