
Secure-by-default demo lab showing how container hardening (distroless images, non-root, read-only filesystem, runtime-injected secrets) can neutralize a critical Next.js/React Server Actions RCE (CVE-2025-55182 “React2Shell”), with side-by-side safe vs unsafe deployments and exploit logs
Dieses Projekt demonstriert eine kritische Remote-Code-Ausführungs-Sicherheitslücke (RCE) in einer Next.js-Anwendung (insbesondere über Server Actions) und wie Härtung der Infrastruktur den Angriff effektiv neutralisiert, selbst wenn die Code-Sicherheitslücke bestehen bleibt.
Es stellt eine standardmäßige "Unsicher"-Bereitstellung einer gehärteten "Sicher"-Bereitstellung gegenüber, die Distroless-Images und schreibgeschützte Dateisysteme nutzt.
Sicherheitslücken in der Software sind unvermeidlich. Wenn der Code versagt, muss Ihre Infrastruktur verhindern, dass der Angreifer seine Position ausbaut.
Eine kritische RCE (CVE-2025-55182, auch bekannt als React2Shell) existiert in der React Server Components (RSC)-Implementierung, die von Next.js verwendet wird.
spawnSync) ohne Authentifizierung.curl, wget, ls, cat), um Geheimnisse zu stehlen oder Malware herunterzuladen.
child_process.spawnSync() von Node.js. Dieser führt Binärdateien direkt aus, ohne eine Shell (/bin/sh) zu benötigen.chmod +x) und führt sie aus.Die folgenden Protokolle zeigen, wie die Angriffsversuche aus Sicht der Anwendung aussehen. Dieser Kontrast verdeutlicht eindrucksvoll die Wirksamkeit der Sicherheitsmaßnahmen.
logs/server.safe.log)Die Protokolle zeigen wiederholte Fehler (ENOENT).
spawnSync versucht, ls, id, curl auszuführen. Das Distroless-Image hat diese Binärdateien einfach nicht. Es geht nicht nur um eine fehlende Shell; die Werkzeuge selbst sind weg.[Instrumentation] Logging initialized. Writing to: /app/logs/server.safe.log
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"1. Write Initial Chunk to /tmp/hello_test","success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"2. Verify Binary was Written","verification":{...},"success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"4. Execute Binary","stdout":"","stderr":"","error_obj":{"message":"spawnSync /tmp/hello_test EACCES","code":"EACCES"},"success":true}`'
logs/server.unsafe.log)Die Protokolle bestätigen erfolgreiche Befehlsausführung und Dateisystemmanipulation.
[Instrumentation] Logging initialized. Writing to: /app/logs/server.unsafe.log
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"1. Write Initial Chunk to /tmp/hello_test","success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"4. Execute Binary","stdout":"Hello from Go binary!\\n","stderr":"","error_obj":null,"success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"command":"id","args":[],"stdout":"uid=0(root) gid=0(root) ...","stderr":"","status":0,"signal":null}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"command":"cat","args":["/app/.env"],"stdout":"","stderr":"cat: can\'t open \'/app/.env\': No such file or directory\n","status":1,"signal":null}`'
(Hinweis: In den unsicheren Protokollen schlägt cat /app/.env oben fehl, weil die Datei im Stammverzeichnis .env heißt, aber ls -la in den vollständigen Protokollen würde die Verzeichnisstruktur offenbaren.)
Versuch, standardmäßige Shell-Befehle auszuführen.
id, ls, cat .env ausführen und auf sensible Daten zugreifen.spawnSync /bin/sh ENOENT. Es gibt keine Shell, um Befehle auszuführen.Versuch, die "fehlenden Werkzeuge" durch Hochladen einer benutzerdefinierten Binärdatei zu umgehen.
/tmp/malware.chmod +x aus.EROFS: read-only file system.Kann ein Angreifer eine Binärdatei in eine Variable laden und direkt aus dem Speicher ausführen?
global.payload = "...") verketten und dann ausführen.child_process-Funktionen (spawn, exec) erfordern einen Dateipfad. Sie können keinen Puffer oder String direkt ausführen.memfd_create (einen Syscall zum Erstellen einer anonymen Datei im RAM).memfd_create nicht nativ bereit. Der Zugriff darauf würde ein C++-Addon (wie ffi-napi) erfordern, das in node_modules vorinstalliert sein müsste.gcc, make) fehlen, kann ein Angreifer dieses Addon nicht spontan erstellen."Distroless"-Images enthalten nur Ihre Anwendung und ihre Laufzeitabhängigkeiten. Sie enthalten keine Paketmanager, Shells oder standardmäßige UNIX-Werkzeuge.
ls), Dateien herunterladen (curl) oder leicht Berechtigungen eskalieren.Konfigurieren Sie Ihre Container-Laufzeitumgebung so, dass das Stammdateisystem schreibgeschützt eingehängt wird.
docker-compose.yml:
read_only: true
tmpfs:
- /tmp:noexec # KRITISCH: Ausführung explizit blockieren!
/tmp schreiben kann (Schreiben erfolgreich), die Ausführung jedoch mit EACCES (Zugriff verweigert) aufgrund des noexec-Flags fehlschlägt. Dies bringt Funktionalität (beschreibbares tmp) mit Sicherheit in Einklang.Versenden Sie .env-Dateien nicht in Ihren Container-Images. Wenn ein Angreifer Dateien lesen kann (z. B. cat .env), sind Ihre Geheimnisse kompromittiert.
environment-Schlüssel von Docker).Starten Sie die Umgebung:
Sowohl die sichere als auch die unsichere Anwendung sind in einer einzigen docker-compose.yml-Datei definiert.
docker compose up --build -d
Führen Sie die Exploits aus: Sie können die Exploits gegen die spezifischen Ports ausführen, um den Unterschied zu sehen.
Ziel: Unsichere App (Port 3001):
# 1. Standard RCE (LotL) - ERFOLGREICH
python exploit/poc.py http://localhost:3001
# 2. Erweiterter Angriff (BYOL) - ERFOLGREICH
python exploit/poc_advanced.py http://localhost:3001
Ziel: Sichere App (Port 3000):
# 1. Standard RCE (LotL) - FEHLGESCHLAGEN (ENOENT)
python exploit/poc.py http://localhost:3000
# 2. Erweiterter Angriff (BYOL) - FEHLGESCHLAGEN (EACCES/EROFS)
python exploit/poc_advanced.py http://localhost:3000
Aufräumen:
docker compose down
| Merkmal | ❌ Unsicheres Umfeld (Port 3001) | ✅ Sicheres Umfeld (Port 3000) |
|---|
| Basis-Image | node:20-alpine (Enthält ls, curl, wget, etc.) | gcr.io/distroless/nodejs20-debian12 (Kein Shell, keine Werkzeuge) |
| Dateisystem | Beschreibbar (Standard-Docker-Voreinstellung) | Schreibgeschützt (read_only: true) |
| Geheimnisse | .env-Datei auf Disk (Anfällig für cat .env) | Umgebungsvariablen (Zur Laufzeit injiziert) |
| Benutzer | root (Standard) | Nicht-root (Durch Distroless erzwungen) |