CVE-2021-21316: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
value: 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.
CWE 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.
CVSS v3 base score: 7.8
Published: 2021-02-16T18:15Z
Repository: https://github.com/SAP/less-openui5
Language: JavaScript
Files changed: README.md, lib/thirdparty/less/README.md, lib/thirdparty/less/env.js, lib/thirdparty/less/functions.js, lib/thirdparty/less/index.js, lib/thirdparty/less/lessc_helper.js, lib/thirdparty/less/parser.js, lib/thirdparty/less/tree/quoted.js, test/test.js
Commit date: 2021-01-29 10:52:59 +0100
Commit message: [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.

Diff: diff --git a/README.md b/README.md
index 2520a41..3a25eca 100644
--- a/README.md
+++ b/README.md
@@ -147,11 +147,14 @@ lib2
 ##### parser
 
 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
 
 Type `object`
 
diff --git a/lib/thirdparty/less/README.md b/lib/thirdparty/less/README.md
index f49506d..b35b939 100644
--- a/lib/thirdparty/less/README.md
+++ b/lib/thirdparty/less/README.md
@@ -1,5 +1,9 @@
 # less.js
 
 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.
diff --git a/lib/thirdparty/less/env.js b/lib/thirdparty/less/env.js
index 10c7302..d7e48c9 100644
--- a/lib/thirdparty/less/env.js
+++ b/lib/thirdparty/less/env.js
@@ -12,11 +12,13 @@
         'insecure',         // option - whether to allow imports from insecure ssl hosts
         'dumpLineNumbers',  // option - whether to dump line numbers
         '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.
     ];
 
diff --git a/lib/thirdparty/less/functions.js b/lib/thirdparty/less/functions.js
index 8c0f103..34dc356 100644
--- a/lib/thirdparty/less/functions.js
+++ b/lib/thirdparty/less/functions.js
@@ -211,11 +211,15 @@ tree.functions = {
         } else {
             return dark;
         }
     },
     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"));
     },
     '%': function (quoted /* arg, arg, ...*/) {
diff --git a/lib/thirdparty/less/index.js b/lib/thirdparty/less/index.js
index a084b02..c90c809 100644
--- a/lib/thirdparty/less/index.js
+++ b/lib/thirdparty/less/index.js
@@ -110,11 +110,13 @@ require('./tree/alpha');
 require('./tree/import');
 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');
 require('./tree/media');
 require('./tree/unicode-descriptor');
diff --git a/lib/thirdparty/less/lessc_helper.js b/lib/thirdparty/less/lessc_helper.js
index 76355eb..f3b9208 100644
--- a/lib/thirdparty/less/lessc_helper.js
+++ b/lib/thirdparty/less/lessc_helper.js
@@ -29,11 +29,13 @@ var lessc_helper = {
         console.log("  -h, --help               Print help (this message) and exit.");
         console.log("  --include-path=PATHS     Set include paths. Separated by `:'. Use `;' on Windows.");
         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.");
         console.log("  --insecure               Allow imports from insecure https hosts.");
         console.log("  -v, --version            Print version number and exit.");
diff --git a/lib/thirdparty/less/parser.js b/lib/thirdparty/less/parser.js
index ab53dd4..faa0653 100644
--- a/lib/thirdparty/less/parser.js
+++ b/lib/thirdparty/less/parser.js
@@ -991,31 +991,27 @@ less.Parser = function Parser(env) {
                     if (ud) {
                         return new(tree.UnicodeDescriptor)(ud[0]);
                     }
                 },
 
+                /* 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 */
             },
 
             //
             // The variable part of a variable definition. Used in the `rule` parser
             //
diff --git a/lib/thirdparty/less/tree/javascript.js b/lib/thirdparty/less/tree/javascript.js
deleted file mode 100644
index 30a8559..0000000
--- a/lib/thirdparty/less/tree/javascript.js
+++ /dev/null
@@ -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'));
-
diff --git a/lib/thirdparty/less/tree/quoted.js b/lib/thirdparty/less/tree/quoted.js
index 131bfad..13f653d 100644
--- a/lib/thirdparty/less/tree/quoted.js
+++ b/lib/thirdparty/less/tree/quoted.js
@@ -20,11 +20,17 @@ tree.Quoted.prototype = {
     },
     toCSS: tree.toCSS,
     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();
         });
         return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index, this.currentFileInfo);
diff --git a/test/test.js b/test/test.js
index c5fd7a8..8a3f3ee 100644
--- a/test/test.js
+++ b/test/test.js
@@ -417,10 +417,118 @@ describe("error handling", function() {
 		}, function(err) {
 			assert.equal(err.message, "compiler.sourceMap option is not supported!");
 			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) {
 	const lessFilename = "test/fixtures/rtl/" + filename + ".less";
 	const cssFilename = "test/expected/rtl/" + filename + ".css";

Vulnerable code: **Note:** Default of `relativeUrls` option is changed from `false` to `true`.
        'javascriptEnabled',// option - whether JavaScript is enabled. if undefined, defaults to true
        return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str);
require('./tree/javascript');
        console.log("  --no-js                  Disable JavaScript in less files");
                // JavaScript code to be evaluated
                    var str, j = i, e;
                    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);
                    }
(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'));

            return new(tree.JavaScript)(exp, that.index, true).eval(env).value;
Fixed code: 
**Note**
- Default of `relativeUrls` option is changed from `false` to `true`.
- Option `javascriptEnabled` has been removed. JavaScript is always disabled and cannot be enabled.

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.
        /* BEGIN MODIFICATION */
        // Removed 'javascriptEnabled'
        /* END MODIFICATION */
        /* BEGIN MODIFICATION */
        // Removed handling of tree.JavaScript
        return new(tree.Anonymous)(str);
        /* END MODIFICATION */

/* BEGIN MODIFICATION */
// Removed require('./tree/javascript');
/* END MODIFICATION */
        /* BEGIN MODIFICATION */
        // Removed --no-js option
        /* END MODIFICATION */
                /* BEGIN MODIFICATION */
                // Removed support for javascript

                // JavaScript code (disabled)
                    var j = i, e;
                    error("You are using JavaScript, which has been disabled.");
                /* END MODIFICATION */
            /* 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 */

	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);
		});
	});