CVE-2021-21316 (published 2021-02-16T18:15Z)

less-openui5 is an npm package which enables building OpenUI5 themes with Less.js. In less-openui5 before version 0.10., when processing theming resources (i.e. `*.less` files) with less-openui5 that originate from an untrusted source, those resources might contain JavaScript code which will be executed in the context of the build process. While this is a feature of the Less.js library it is an unexpected behavior in the context of OpenUI5 and SAPUI5 development. Especially in the context of UI5 Tooling which relies on less-openui5. An attacker might create a library or theme-library containing a custom control or theme, hiding malicious JavaScript code in one of the .less files. Refer to the referenced GHSA-3crj-w4f5-gwh4 for examples. Starting with Less.js version 3.0.0, the Inline JavaScript feature is disabled by default. less-openui5 however currently uses a fork of Less.js v1.6.3. Note that disabling the Inline JavaScript feature in Less.js versions 1.x, still evaluates code has additional double codes around it. We decided to remove the inline JavaScript evaluation feature completely from the code of our Less.js fork. This fix is available in less-openui5 version 0.10.0.

Severity: MEDIUM
CVSS v2 base score: 6.8 (AV:N/AC:M/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 7.8, HIGH (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         

Fix information:
repo_url: https://github.com/SAP/less-openui5
commit_details: repo_url: https://github.com/SAP/less-openui5
author: Matthias Oßwald
author_date: 2021-01-29 10:48:58+01:00
msg: [BREAKING] Security: Disable JavaScript execution in Less.js

Execution of JavaScript code located in *.less files is unexpected in
the context of OpenUI5 / SAPUI5 development and poses a security threat.

BREAKING CHANGE:
Parser option `javascriptEnabled` has been removed. JavaScript is always
disabled and cannot be enabled.
repository: repo_url: https://github.com/SAP/less-openui5
repo_name: SAP/less-openui5
description: Build OpenUI5 themes with Less.js.
repo_language: JavaScript
owner: SAP
file_changes: filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -149,7 +149,10 @@ lib2
 Type: `object`
 
 Options for the [less](http://lesscss.org) parser (`less.Parser`).  
-**Note:** Default of `relativeUrls` option is changed from `false` to `true`.
+
+**Note**
+- Default of `relativeUrls` option is changed from `false` to `true`.
+- Option `javascriptEnabled` has been removed. JavaScript is always disabled and cannot be enabled.
 
 ##### compiler
 

programming_language: Markdown
filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -3,3 +3,7 @@
 This folder contains the `lib/less` sub-folder of [v1.6.3](https://github.com/less/less.js/tree/v1.6.3/lib/less) of the [less.js project](https://github.com/less/less.js) with commit [`ccd8ebbfdfa300b6e748e8d7c12e3dbb0efd8371`](https://github.com/less/less.js/commit/ccd8ebbfdfa300b6e748e8d7c12e3dbb0efd8371) applied on top of it to resolve https://github.com/SAP/less-openui5/issues/24.
 
 The files `browser.js` and `rhino.js` have been removed, as they are not relevant for the Node.js implementation.
+
+The file `tree/javascript.js` has been removed to disable JavaScript execution.
+
+Modifications within the files are marked with `/* BEGIN MODIFICATION */` and `/* END MODIFICATION */` comments.

programming_language: Markdown
filename: env.js
change_type: ModificationType.MODIFY
diff: @@ -14,7 +14,9 @@
         'compress',         // option - whether to compress
         'processImports',   // option - whether to process imports. if false then imports will not be imported
         'syncImport',       // option - whether to import synchronously
-        'javascriptEnabled',// option - whether JavaScript is enabled. if undefined, defaults to true
+        /* BEGIN MODIFICATION */
+        // Removed 'javascriptEnabled'
+        /* END MODIFICATION */
         'mime',             // browser only - mime type for sheet import
         'useFileCache',     // browser only - whether to use the per file session cache
         'currentFileInfo'   // information about the current file - for error reporting and importing and making urls relative etc.

programming_language: JavaScript
filename: functions.js
change_type: ModificationType.MODIFY
diff: @@ -213,7 +213,11 @@ tree.functions = {
         }
     },
     e: function (str) {
-        return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str);
+        /* BEGIN MODIFICATION */
+        // Removed handling of tree.JavaScript
+        return new(tree.Anonymous)(str);
+        /* END MODIFICATION */
+
     },
     escape: function (str) {
         return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29"));

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -112,7 +112,9 @@ require('./tree/mixin');
 require('./tree/comment');
 require('./tree/anonymous');
 require('./tree/value');
-require('./tree/javascript');
+/* BEGIN MODIFICATION */
+// Removed require('./tree/javascript');
+/* END MODIFICATION */
 require('./tree/assignment');
 require('./tree/condition');
 require('./tree/paren');

programming_language: JavaScript
filename: lessc_helper.js
change_type: ModificationType.MODIFY
diff: @@ -31,7 +31,9 @@ var lessc_helper = {
         console.log("  -M, --depends            Output a makefile import dependency list to stdout");
         console.log("  --no-color               Disable colorized output.");
         console.log("  --no-ie-compat           Disable IE compatibility checks.");
-        console.log("  --no-js                  Disable JavaScript in less files");
+        /* BEGIN MODIFICATION */
+        // Removed --no-js option
+        /* END MODIFICATION */
         console.log("  -l, --lint               Syntax check only (lint).");
         console.log("  -s, --silent             Suppress output of error messages.");
         console.log("  --strict-imports         Force evaluation of imports.");

programming_language: JavaScript
filename: parser.js
change_type: ModificationType.MODIFY
diff: @@ -993,27 +993,23 @@ less.Parser = function Parser(env) {
                     }
                 },
 
+                /* BEGIN MODIFICATION */
+                // Removed support for javascript
+
                 //
-                // JavaScript code to be evaluated
+                // JavaScript code (disabled)
                 //
                 //     `window.location.href`
                 //
                 javascript: function () {
-                    var str, j = i, e;
+                    var j = i, e;
 
                     if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings
                     if (input.charAt(j) !== '`') { return; }
-                    if (env.javascriptEnabled !== undefined && !env.javascriptEnabled) {
-                        error("You are using JavaScript, which has been disabled.");
-                    }
-
-                    if (e) { $char('~'); }
 
-                    str = $re(/^`([^`]*)`/);
-                    if (str) {
-                        return new(tree.JavaScript)(str[1], i, e);
-                    }
+                    error("You are using JavaScript, which has been disabled.");
                 }
+                /* END MODIFICATION */
             },
 
             //

programming_language: JavaScript
filename: javascript.js
change_type: ModificationType.DELETE
diff: @@ -1,58 +0,0 @@
-(function (tree) {
-
-tree.JavaScript = function (string, index, escaped) {
-    this.escaped = escaped;
-    this.expression = string;
-    this.index = index;
-};
-tree.JavaScript.prototype = {
-    type: "JavaScript",
-    eval: function (env) {
-        var result,
-            that = this,
-            context = {};
-
-        var expression = this.expression.replace(/@\{([\w-]+)\}/g, function (_, name) {
-            return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env));
-        });
-
-        try {
-            expression = new(Function)('return (' + expression + ')');
-        } catch (e) {
-            throw { message: "JavaScript evaluation error: " + e.message + " from `" + expression + "`" ,
-                    index: this.index };
-        }
-
-        var variables = env.frames[0].variables();
-        for (var k in variables) {
-            if (variables.hasOwnProperty(k)) {
-                /*jshint loopfunc:true */
-                context[k.slice(1)] = {
-                    value: variables[k].value,
-                    toJS: function () {
-                        return this.value.eval(env).toCSS();
-                    }
-                };
-            }
-        }
-
-        try {
-            result = expression.call(context);
-        } catch (e) {
-            throw { message: "JavaScript evaluation error: '" + e.name + ': ' + e.message.replace(/["]/g, "'") + "'" ,
-                    index: this.index };
-        }
-        if (typeof(result) === 'number') {
-            return new(tree.Dimension)(result);
-        } else if (typeof(result) === 'string') {
-            return new(tree.Quoted)('"' + result + '"', result, this.escaped, this.index);
-        } else if (Array.isArray(result)) {
-            return new(tree.Anonymous)(result.join(', '));
-        } else {
-            return new(tree.Anonymous)(result);
-        }
-    }
-};
-
-})(require('../tree'));
-

programming_language: unknown
filename: quoted.js
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,13 @@ tree.Quoted.prototype = {
     eval: function (env) {
         var that = this;
         var value = this.value.replace(/`([^`]+)`/g, function (_, exp) {
-            return new(tree.JavaScript)(exp, that.index, true).eval(env).value;
+            /* BEGIN MODIFICATION */
+            // Removed support for javascript
+            const error = new Error("You are using JavaScript, which has been disabled.");
+            error.index = that.index;
+            error.type = "Syntax";
+            throw error;
+            /* END MODIFICATION */
         }).replace(/@\{([\w-]+)\}/g, function (_, name) {
             var v = new(tree.Variable)('@' + name, that.index, that.currentFileInfo).eval(env, true);
             return (v instanceof tree.Quoted) ? v.value : v.toCSS();

programming_language: JavaScript
filename: test.js
change_type: ModificationType.MODIFY
diff: @@ -419,6 +419,114 @@ describe("error handling", function() {
 			assert.ok(err);
 		});
 	});
+
+	it("should throw error when using inline JavaScript", function() {
+		const lessInput = `.rule {
+	@var: \`(function(){ return "Cat"; })()\`;
+	color: @var;
+}`;
+		return new Builder().build({
+			lessInput
+		}).then(function(res) {
+			// no resolve
+			assert.ok(false, `Expected build to fail but finished with content:\n${res.css}`);
+		}, function(err) {
+			assert.equal(err.message, "You are using JavaScript, which has been disabled.");
+			assert.ok(err);
+		});
+	});
+
+	it("should throw error when using quoted inline JavaScript", function() {
+		const lessInput = `.rule {
+	@var: "\`(function(){ return 'Cat'; })()\`";
+	color: @var;
+}`;
+		return new Builder().build({
+			lessInput
+		}).then(function(res) {
+			// no resolve
+			assert.ok(false, `Expected build to fail but finished with content:\n${res.css}`);
+		}, function(err) {
+			assert.equal(err.message, "You are using JavaScript, which has been disabled.");
+			assert.ok(err);
+		});
+	});
+
+	it("should throw error when using inline JavaScript with parser option javascriptEnabled: true", function() {
+		const lessInput = `.rule {
+	@var: \`(function(){ return "Cat"; })()\`;
+	color: @var;
+}`;
+		return new Builder().build({
+			lessInput,
+			parser: {
+				javascriptEnabled: true
+			}
+		}).then(function(res) {
+			// no resolve
+			assert.ok(false, `Expected build to fail but finished with content:\n${res.css}`);
+		}, function(err) {
+			assert.equal(err.message, "You are using JavaScript, which has been disabled.");
+			assert.ok(err);
+		});
+	});
+
+	it("should throw error when using quoted inline JavaScript with parser option javascriptEnabled: true", function() {
+		const lessInput = `.rule {
+	@var: "\`(function(){ return 'Cat'; })()\`";
+	color: @var;
+}`;
+		return new Builder().build({
+			lessInput,
+			parser: {
+				javascriptEnabled: true
+			}
+		}).then(function(res) {
+			// no resolve
+			assert.ok(false, `Expected build to fail but finished with content:\n${res.css}`);
+		}, function(err) {
+			assert.equal(err.message, "You are using JavaScript, which has been disabled.");
+			assert.ok(err);
+		});
+	});
+
+	it("should throw error when using inline JavaScript with parser option javascriptEnabled: false", function() {
+		const lessInput = `.rule {
+	@var: \`(function(){ return "Cat"; })()\`;
+	color: @var;
+}`;
+		return new Builder().build({
+			lessInput,
+			parser: {
+				javascriptEnabled: false
+			}
+		}).then(function(res) {
+			// no resolve
+			assert.ok(false, `Expected build to fail but finished with content:\n${res.css}`);
+		}, function(err) {
+			assert.equal(err.message, "You are using JavaScript, which has been disabled.");
+			assert.ok(err);
+		});
+	});
+
+	it("should throw error when using quoted inline JavaScript with parser option javascriptEnabled: false", function() {
+		const lessInput = `.rule {
+	@var: "\`(function(){ return 'Cat'; })()\`";
+	color: @var;
+}`;
+		return new Builder().build({
+			lessInput,
+			parser: {
+				javascriptEnabled: false
+			}
+		}).then(function(res) {
+			// no resolve
+			assert.ok(false, `Expected build to fail but finished with content:\n${res.css}`);
+		}, function(err) {
+			assert.equal(err.message, "You are using JavaScript, which has been disabled.");
+			assert.ok(err);
+		});
+	});
 });
 
 function assertLessToRtlCssEqual(filename) {

programming_language: JavaScript