JavaScript-Code dynamisch zur Laufzeit laden und ausführen
Seite 2: Lösung: JavaScript mit vm ausführen
Eine Möglichkeit, JavaScript-Code in einem eigenen Kontext auszuführen, bietet das Modul vm der Standard-Node.js-API über die Methoden createContext() und runInContext(). Die Methode createContext() dient dabei dem Anlegen eines neuen Kontexts: Hier übergeben Entwickler als Parameter ein sogenanntes Sandbox-Objekt, also einen Bereich, auf den sowohl der aufrufende Code als auch der im Kontext aufgerufene Code Zugriff haben. Intern wird durch den Aufruf von createContext() das Sandbox-Objekt mit einer neuen Instanz eines V8-Kontexts verknüpft. Die Methode runInContext() dient dem Ausführen von Code in dem zuvor angelegten Kontext. Dieser Methode übergibt man als ersten Parameter den auszuführenden Code und als zweiten das Sandbox-Objekt:
const vm = require('vm');
const x = 1;
const sandbox = { x: 2 };
vm.createContext(sandbox);
const code = `
x += 20;
y = 25;
`;
vm.runInContext(code, sandbox);
console.log(sandbox.x); // 22
console.log(sandbox.y); // 25
console.log(x); // 1
// console.log(y); // not defined
Um die Schritte für das Erstellen eines neuen Kontexts und das Ausführen von Code in diesem Kontext direkt in einem Schritt zu machen, können Entwickler alternativ die Methode runInNewContext() verwenden:
const vm = require('vm');
const x = 1;
const sandbox = { x: 2 };
const code = `
x += 20;
y = 25;
`;
vm.runInNewContext(code, sandbox);
console.log(sandbox.x); // 22
console.log(sandbox.y); // 25
console.log(x); // 1
// console.log(y); // not undefined
Philip Ackermanns JavaScript-Bücher bei Rheinwerk (3 Bilder)

JavaScript – Das umfassende Handbuch
Versucht man nun, Code auszuführen, der beispielsweise versucht, auf das process-Objekt zuzugreifen ...
const vm = require('vm');
const code = `
console.log('Exiting process');
process.exit(0);
`;
console.log('Before executing code');
try {
const sandbox = {};
vm.runInNewContext(code, sandbox);
} catch (error) {
console.error(error);
}
console.log('After executing code');
... kommt es zu einem Fehler, weil innerhalb des erzeugten Kontexts, in dem der geladene Code läuft, kein process-Objekt vorhanden ist:
$ node src/start-vm-new-context.js
Before executing code
evalmachine.<anonymous>:3
process.exit(0);
^
ReferenceError: process is not defined
at evalmachine.<anonymous>:3:3
at Script.runInContext (vm.js:102:20)
at Script.runInNewContext (vm.js:108:17)
at Object.runInNewContext (vm.js:291:38)
at Object.<anonymous> (/Users/cleancoderocker/workspaces/nodejskochbuch/.../src/start-vm-new-context.js:8:4)
at Module._compile (internal/modules/cjs/loader.js:702:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:713:10)
at Module.load (internal/modules/cjs/loader.js:612:32)
at tryModuleLoad (internal/modules/cjs/loader.js:551:12)
After executing code
So weit, so gut. Allerdings ist auch die Ausführung von Code über vm nicht ganz sicher beziehungsweise weist vm eine Schachstelle auf, über die sich Entwickler im Klaren sein sollten. Prinzipiell geht es bei dem dynamischen Laden und Ausführen von (Fremd-)Code ja darum, den Zugriff auf den Kontext des eigenen Codes zu unterbinden. Über vm wird so beispielsweise der Zugriff auf globale Objekte wie process oder console unterbunden.
Dennoch gibt es einen Weg, innerhalb des ausgeführten Codes Zugriff auf diese Objekte zu bekommen. Und zwar über das Sandbox-Objekt, wie anhand des in folgendem Beispiel gezeigten Exploits zu sehen. Wenn man in dem Sandbox-Objekt nämlich andere Objekte als Eigenschaften definiert, kann innerhalb des aufgerufenen Codes über deren constructor-Eigenschaft auf eine Funktion zugegriffen werden, über die dann wiederum Zugriff auf die globalen Objekte (im Beispiel process und console) besteht:
const vm = require('vm');
const sandbox = { someObject: {} };
const code = `
const ForeignObject = someObject.constructor;
const ForeignFunction = ForeignObject.constructor;
const process = ForeignFunction('return process')();
const console = ForeignFunction('return console')();
console.log('Exiting process');
process.exit(0);
`;
console.log('Before executing code');
vm.runInNewContext(code, sandbox);
// Wird nie ausgeführt
console.log('After executing code');
Aus diesem Grund lässt sich auch nicht zur Verwendung des vm-Moduls raten. Zumindest nicht, wenn Entwickler, wie im letzten Beispiel gezeigt, über das Sandbox-Objekt weitere Objekte definieren und dadurch der Exploit ausgespielt werden kann. Auf der sicheren Seite sind sie erst, wenn sie die im nächsten Abschnitt beschriebene Lösung verwenden.