
Laboratoire de démonstration sécurisé par défaut montrant comment le durcissement des conteneurs (images distroless, non-root, système de fichiers en lecture seule, secrets injectés à l'exécution) peut neutraliser une RCE critique Next.js/React Server Actions (CVE-2025-55182 « React2Shell »), avec des déploiements sûrs et non sûrs côte à côte et des journaux d'exploitation
Ce projet démontre une vulnérabilité critique d'exécution de code à distance (RCE) dans une application Next.js (notamment via les Server Actions) et comment le renforcement de l'infrastructure neutralise efficacement l'attaque même lorsque la vulnérabilité du code persiste.
Il oppose un déploiement « Non sécurisé » standard à un déploiement « Sécurisé » durci utilisant des images Distroless et des systèmes de fichiers en lecture seule.
Les vulnérabilités logicielles sont inévitables. Lorsque le code échoue, votre infrastructure doit empêcher l'attaquant d'étendre son emprise.
Une RCE critique (CVE-2025-55182, alias React2Shell) existe dans l'implémentation des React Server Components (RSC) utilisée par Next.js.
spawnSync) sans authentification.curl, wget, ls, cat) pour voler des secrets ou télécharger des malwares.
child_process.spawnSync() de Node.js. Cela exécute des binaires directement, sans nécessiter de shell (/bin/sh).chmod +x) et l'exécute.Les logs suivants montrent à quoi ressemblent les tentatives d'attaque du point de vue de l'application. Ce contraste met vivement en évidence l'efficacité des mesures de sécurité.
logs/server.safe.log)Les logs montrent des échecs répétés (ENOENT).
spawnSync essaie d'exécuter ls, id, curl. L'image Distroless ne possède tout simplement pas ces binaires. Il ne s'agit pas seulement d'un shell manquant ; les outils eux-mêmes sont absents.[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)Les logs confirment une exécution de commande réussie et une manipulation du système de fichiers.
[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}`'
(Remarque : Dans les logs non sécurisés, cat /app/.env échoue ci-dessus car le fichier est nommé .env à la racine, mais ls -la dans les logs complets révélerait la structure des répertoires.)
Tentative d'exécution de commandes shell standard.
id, ls, cat .env et accéder à des données sensibles.spawnSync /bin/sh ENOENT. Il n'y a pas de shell pour exécuter des commandes.Tentative de contournement des « outils manquants » en téléchargeant un binaire personnalisé.
/tmp/malware.chmod +x.EROFS: read-only file system.Un attaquant peut-il charger un binaire dans une variable et l'exécuter directement depuis la mémoire ?
global.payload = "..."), puis l'exécuter.child_process de Node.js (spawn, exec) nécessitent un chemin de fichier. Elles ne peuvent pas exécuter directement un tampon ou une chaîne.memfd_create (un appel système pour créer un fichier anonyme en RAM).memfd_create nativement. Y accéder nécessiterait un addon C++ (comme ffi-napi) préinstallé dans node_modules.gcc, make), un attaquant ne peut pas construire cet addon à la volée.Les images « Distroless » contiennent uniquement votre application et ses dépendances d'exécution. Elles ne contiennent pas de gestionnaires de paquets, de shells ou d'outils UNIX standard.
ls), télécharger des fichiers (curl) ou escalader les privilèges facilement.Configurez votre runtime de conteneur pour monter le système de fichiers racine en lecture seule.
docker-compose.yml :
read_only: true
tmpfs:
- /tmp:noexec # CRITICAL: explicitly block execution!
/tmp (écriture réussie), mais l'exécution échoue avec EACCES (Permission refusée) à cause du drapeau noexec. Cela équilibre fonctionnalité (tmp inscriptible) et sécurité.N'incluez pas de fichiers .env dans vos images de conteneur. Si un attaquant peut lire des fichiers (par exemple, cat .env), vos secrets sont compromis.
environment de Docker).Démarrer l'environnement :
Les applications sécurisée et non sécurisée sont définies dans un seul fichier docker-compose.yml.
docker compose up --build -d
Exécuter les exploits : Vous pouvez exécuter les exploits sur les ports spécifiques pour voir la différence.
Cibler l'application non sécurisée (Port 3001) :
# 1. Standard RCE (LotL) - SUCCEEDS
python exploit/poc.py http://localhost:3001
# 2. Advanced Attack (BYOL) - SUCCEEDS
python exploit/poc_advanced.py http://localhost:3001
Cibler l'application sécurisée (Port 3000) :
# 1. Standard RCE (LotL) - FAILS (ENOENT)
python exploit/poc.py http://localhost:3000
# 2. Advanced Attack (BYOL) - FAILS (EACCES/EROFS)
python exploit/poc_advanced.py http://localhost:3000
Nettoyer :
docker compose down
| Caractéristique | ❌ Environnement non sécurisé (Port 3001) | ✅ Environnement sécurisé (Port 3000) |
|---|
| Image de base | node:20-alpine (Contient ls, curl, wget, etc.) | gcr.io/distroless/nodejs20-debian12 (Pas de shell, pas d'outils) |
| Système de fichiers | Inscriptible (par défaut Docker standard) | Lecture seule (read_only: true) |
| Secrets | Fichier .env sur disque (Vulnérable à cat .env) | Variables d'environnement (Injectées à l'exécution) |
| Utilisateur | root (Par défaut) | Non-root (Appliqué par Distroless) |