CVE-2021-21316 (JavaScript, published 2021-02-16T18:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 7.8)
Weakness: CWE-74 \u2014 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
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.
Repository: https://github.com/SAP/less-openui5
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 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.


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