Zum Hauptinhalt springen

JavaScript-Codeausführung in der Sandbox

Dieses Dokument beschreibt, wie unsere Laufzeit-Sicherheits-Suite automatisch beliebige Funktionen von NPM-Paketen erkennt, untersucht und innerhalb einer isolierten Sandbox-Umgebung ausführt.

Zielsetzung

Das Hauptziel des Universal Package Runner ist nicht die Perfektion von Unit-Tests, sondern die Erreichbarkeit zur Laufzeit (Runtime Reachability). Indem wir jeden zugänglichen Export eines NPM-Pakets mit intelligenten, synthetischen Parametern aufrufen, erzwingen wir die Auslösung versteckter oder verzögerter schädlicher Payloads während der Laufzeit. Dies ermöglicht die Überwachung von Systemaufrufen, Netzwerkaktivitäten und Prozessstarts in Echtzeit.

1. Warum dieser Ansatz?​

Statische Codeanalyse allein reicht oft nicht aus, um moderne Supply-Chain-Angriffe, verschleierte Payloads oder dynamisch ausgewertete Skripte (eval, Function(), verschleierte Strings) zu erkennen.

Um diese Einschränkungen zu überwinden, haben wir uns für die dynamische Laufzeituntersuchung mittels rekursiver Baumdurchquerung entschieden:

  • Payload-Ausführung beim Aufruf: Schadroutinen (wie das Abgreifen von Zugangsdaten oder Reverse Shells) werden häufig unmittelbar beim Aufruf der Funktion ausgeführt – selbst wenn die Funktion später aufgrund ungültiger Parameter einen TypeError auslöst.
  • Keine manuellen Testumgebungen: Anstatt manuell geschriebene Test-Suiten für Tausende einzelner NPM-Pakete zu pflegen, passt sich unser automatisierter Runner dynamisch an jede Paketschnittstelle an.
  • Code-Erreichbarkeit wichtiger als reine Erfolgsquoten: Ein Funktionsaufruf, der zu einem ❌ FAIL führt, erfüllt dennoch unseren Sicherheitsauftrag: Der zugrundeliegende Codepfad wurde ausgeführt, und etwaige schädliche Nebeneffekte wurden für unsere Sandbox-Hooks sichtbar gemacht.
Wichtige Erkenntnis

Bei Sicherheitstests ist die Ausführungsabdeckung (Execution Coverage) weitaus wichtiger als die Erfolgsquoten der Funktionen. Eine Funktion, die nach dem Absetzen einer unbefugten HTTP-Anfrage abstürzt, hat ihr schädliches Verhalten dennoch erfolgreich gegenüber der Sandbox offengelegt.

2. Funktionsweise​

Der Runner arbeitet mit einer mehrstufigen Pipeline, die darauf ausgelegt ist, komplexe Paketstrukturen sicher zu durchlaufen:

┌─────────────────────────────────────────────────────────────┐
│ 1. PAKET-AUFLÖSUNG │
│ Dynamische Auflösung und Laden des Zielpakets │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 2. REKURSIVE EXPORT-ANALYSE │
│ • Durchlaufen des Objekt-Graphen via Reflect.ownKeys() │
│ • Vermeidung von Zyklen durch Tracking via WeakSet │
│ • Sichere Auswertung von Property-Gettern & Tiefenbegrenzung│
└──────────────────────────────┬────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 3. SYNTHESE VON PARAMETERN & BLUEPRINTS │
│ • Analyse der Funktionssignatur & Bestimmung der Stelligkeit│
│ • Musterbasierte Argumentgenerierung (Callbacks, Pfade) │
│ • Bestimmung der Kontextbindung (DIRECT/METHOD/CTOR) │
└──────────────────────────────┬────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 4. GESCHÜTZTE AUSFÜHRUNG IN DER SANDBOX │
│ • Ausführung mit Überschreibung von process.exit() & Fehlern│
│ • Asynchrone Promise-Auflösung & Lifecycle-Abschluss │
└───────────────────────────────────────────────────────────────┘

Phase 1: Rekursives Durchlaufen des Pakets​

Der Scanner akzeptiert den Namen eines beliebigen NPM-Zielpakets und durchläuft rekursiv dessen Export-Graphen (einschließlich verschachtelter Objekte, exportierter Klassen und statischer Hilfsmethoden). Ein WeakSet protokolliert die bereits besuchten Referenzen, um zirkuläre Objektverweise zu verhindern.

Phase 2: Parameter-Heuristik & Blueprint-Generierung​

Für jede gefundene Funktion analysiert der Parser deren Signatur:

  1. Bestimmung der Stelligkeit (Arity): Auswertung von fn.length und Parameter-Token mittels Regex- oder AST-Analyse.
  2. Kontextbindung: Identifizierung, ob es sich bei der Funktion um einen direkten Export (pkg.fn), eine Methode mit this-Bindung (pkg.sub.method) oder eine Konstruktor-Klasse (new pkg.Class) handelt. 3. Intelligente Argument-Synthese: Erzeugt positionsbasierte Dummy-Argumente auf der Grundlage von Benennungsmustern für Parameter (z. B. Übergabe von Dummy-Callbacks für cb, Mock-URLs für path oder Resolve-Wrapper für action).

Phase 3: Isolierte Ausführung in der Sandbox​

Die Zielfunktion wird unter Verwendung des entsprechenden Ausführungs-Blueprints (DIRECT, METHOD oder CONSTRUCTOR) aufgerufen. Asynchrone Promises werden abgewartet, um asynchrone Ablehnungen (Rejections) sicher zu erfassen.

3. Kernfunktionen & Sicherheitsmerkmale​

Unser Executor umfasst verschiedene Resilienzmechanismen, die sicherstellen, dass der Test-Runner auch bei der Ausführung von instabilem oder schädlichem Code betriebsbereit bleibt:

FunktionMechanismusWarum
Abfangen von process.exitÜberschreibt process.exit() mit einem No-Op-HandlerVerhindert, dass getestete CLI-Pakete den Runner vorzeitig beenden.
Globale Hooks für RejectionsÜberwacht uncaughtException und unhandledRejectionVerhindert, dass asynchrone Hintergrundfehler den Node.js-Prozess zum Absturz bringen.
Sichere Getter-AuswertungWertet Property-Getter mittels try...catch-Blöcken ausErmöglicht Chaining-APIs im Builder-Stil (z. B. chalk.red.bold) ohne Laufzeitfehler.
Begrenzung der RekursionstiefeErzwingt eine konfigurierbare MAX_DEPTH-BeschränkungVerhindert Endlosschleifen durch selbstreferenzierende Builder-Muster.
Beibehaltung der this-BindungLöst Referenzen auf das übergeordnete Objekt vor dem Aufruf von .apply() aufStellt sicher, dass Objektmethoden weiterhin Zugriff auf interne Eigenschaften haben.
Prozess-Isolation

Obwohl der Runner das Beenden des Prozesses intern unterbindet, ist für eine vollständige Kapselung die Ausführung in einem dedizierten, isolierten Container (z. B. Docker, Firecracker oder einer gVisor-Sandbox) erforderlich, um Änderungen am Host-System zu verhindern.

Wir führen dies in unserem Sandbox-System aus.

4. Ausführungsbeispiel & Ausgabeformat​

Im Folgenden sehen Sie ein Beispiel, bei dem der Runner das Paket figlet analysiert und ausführt:

Ausführungsbefehl​

node UniversalPackageRunner.js figlet

Konsolenausgabe​

Diese ist auf Englisch, da das Programm nur in dieser Sprache verfügbar ist.

🔍 Discovered 13 function(s) in "figlet".
🚀 Executing functions...

✅ SUCCESS [DIRECT]: pkg("test_string", ()=>{}, ()=>{})
✅ SUCCESS [DIRECT]: pkg.text("test_string", ()=>{}, ()=>{})
✅ SUCCESS [SUCCESS]: pkg.textSync("test_string", {"testKey":"testValue","debug":true})
✅ SUCCESS [DIRECT]: pkg.metadata("Standard", ()=>{})
✅ SUCCESS [DIRECT]: pkg.defaults({"testKey":"testValue","debug":true})
❌ FAIL [DIRECT]: pkg.parseFont("Standard", "test_string", 1)
↳ Reason: FIGlet header contains invalid values.
✅ SUCCESS [DIRECT]: pkg.loadedFonts()
✅ SUCCESS [DIRECT]: pkg.clearLoadedFonts()
❌ FAIL [DIRECT]: pkg.loadFont("test_string", ()=>{})
↳ Reason: ENOENT: no such file or directory, open '.../fonts/test_string.flf'
✅ SUCCESS [DIRECT]: pkg.loadFontSync("Standard")
✅ SUCCESS [DIRECT]: pkg.preloadFonts("Standard", ()=>{})

===================================
📊 SUMMARY FOR FIGLET
Successful: 11
Failed: 2
===================================
Interpretation der Fehler

Beachten Sie, dass pkg.loadFont mit ENOENT fehlgeschlagen ist, da "test_string" keine echte Schriftartdatei ist. Die Funktion wurde jedoch erfolgreich aufgerufen; das bedeutet, dass jeder potenzielle Dateizugriff oder Netzwerkanfrage innerhalb von loadFont von unserer Sandbox-Überwachungsschicht erfasst wurde.

5. Verzeichnisstruktur & Modularisierung​

Der Runner ist in kleine Module mit jeweils einer einzigen Zuständigkeit (Single-Responsibility-Prinzip) unterteilt:

├── UniversalPackageRunner.js # Haupt-Orchestrator und Ausführungsschleife
└── utils/
├── GenerateFakeArg.js # Intelligenter Generator für synthetische Argumente
└── GetParamNames.js # Parser für Funktionssignaturen und Parameter

Aufgaben der Module​

  • UniversalPackageRunner.js: Ermittelt das Zielpaket, steuert den rekursiven Scanner, verwaltet Prozess-Overrides und protokolliert Zusammenfassungen der Ausführung.
  • GenerateFakeArg.js: Ordnet Parameter-String-Muster entsprechenden Laufzeittypen (Callbacks, Booleans, Objekte, Arrays, Ganzzahlen) zu.
  • GetParamNames.js: Analysiert Funktions-Strings, um Parameternamen für eine präzise Argumentzuordnung zu extrahieren.