
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
Этот проект демонстрирует критическую уязвимость удаленного выполнения кода (RCE) в приложении Next.js (конкретно через Server Actions) и то, как усиление инфраструктуры эффективно нейтрализует атаку, даже если кодовая уязвимость остается.
В нем сравнивается стандартное «Небезопасное» развертывание с усиленным «Безопасным» развертыванием с использованием образов Distroless и файловых систем только для чтения.
Уязвимости ПО неизбежны. Когда код дает сбой, ваша инфраструктура должна помешать атакующему расширить свое присутствие.
Критическая RCE (CVE-2025-55182, также известная как React2Shell) существует в реализации React Server Components (RSC), используемой Next.js.
spawnSync) без аутентификации.curl, wget, ls, cat) для кражи секретов или загрузки вредоносного ПО.
child_process.spawnSync() из Node.js. Он выполняет бинарные файлы напрямую, без необходимости в оболочке (/bin/sh).chmod +x) и запускает.Следующие логи показывают, как попытки атаки выглядят с точки зрения приложения. Это контрастное сравнение ярко демонстрирует эффективность мер безопасности.
logs/server.safe.log)Логи показывают повторяющиеся ошибки (ENOENT).
spawnSync пытается запустить ls, id, curl. В образе Distroless этих бинарных файлов просто нет. Дело не только в отсутствии оболочки; сами инструменты отсутствуют.[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)Логи подтверждают успешное выполнение команд и манипуляции с файловой системой.
[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}`'
(Примечание: в небезопасных логах cat /app/.env завершается ошибкой выше, потому что файл назван .env в корне, но ls -la в полных логах раскрыл бы структуру каталогов.)
Попытка выполнить стандартные команды оболочки.
id, ls, cat .env и получить доступ к конфиденциальным данным.spawnSync /bin/sh ENOENT. Нет оболочки для выполнения команд.Попытка обойти «отсутствие инструментов» путем загрузки собственного бинарного файла.
/tmp/malware.chmod +x.EROFS: read-only file system.Может ли атакующий загрузить бинарный файл в переменную и выполнить его напрямую из памяти?
global.payload = "..."), затем выполнить её.child_process в Node.js (spawn, exec) требуют путь к файлу. Они не могут выполнить буфер или строку напрямую.memfd_create (системный вызов для создания анонимного файла в ОЗУ).memfd_create нативно. Доступ к нему потребовал бы C++-аддон (например, ffi-napi), предустановленный в node_modules.gcc, make), атакующий не может собрать этот аддон на лету.Образы «Distroless» содержат только ваше приложение и его зависимости времени выполнения. Они не содержат менеджеров пакетов, оболочек или стандартных инструментов UNIX.
ls), скачать файлы (curl) или легко повысить привилегии.Настройте среду выполнения контейнера так, чтобы корневая файловая система была смонтирована только для чтения.
docker-compose.yml:
read_only: true
tmpfs:
- /tmp:noexec # КРИТИЧЕСКИ ВАЖНО: явно заблокировать выполнение!
/tmp (запись успешна), но выполнение завершается ошибкой EACCES (отказано в доступе) из-за флага noexec. Это балансирует функциональность (записываемый tmp) и безопасность.Не помещайте файлы .env в образы контейнеров. Если атакующий может читать файлы (например, cat .env), ваши секреты скомпрометированы.
environment Docker).Запустите окружение:
И безопасное, и небезопасное приложения определены в одном файле docker-compose.yml.
docker compose up --build -d
Запустите эксплойты: Вы можете запустить эксплойты против конкретных портов, чтобы увидеть разницу.
Нацеливание на небезопасное приложение (порт 3001):
# 1. Стандартное RCE (LotL) - УСПЕХ
python exploit/poc.py http://localhost:3001
# 2. Продвинутая атака (BYOL) - УСПЕХ
python exploit/poc_advanced.py http://localhost:3001
Нацеливание на безопасное приложение (порт 3000):
# 1. Стандартное RCE (LotL) - НЕ УДАЕТСЯ (ENOENT)
python exploit/poc.py http://localhost:3000
# 2. Продвинутая атака (BYOL) - НЕ УДАЕТСЯ (EACCES/EROFS)
python exploit/poc_advanced.py http://localhost:3000
Очистка:
docker compose down
| Характеристика | ❌ Небезопасное окружение (Порт 3001) | ✅ Безопасное окружение (Порт 3000) |
|---|
| Базовый образ | node:20-alpine (содержит ls, curl, wget и т.д.) | gcr.io/distroless/nodejs20-debian12 (нет оболочки, нет инструментов) |
| Файловая система | Записываемая (стандартное значение Docker по умолчанию) | Только для чтения (read_only: true) |
| Секреты | Файл .env на диске (уязвим для cat .env) | Переменные окружения (внедряются во время выполнения) |
| Пользователь | root (по умолчанию) | Не-root (обеспечивается Distroless) |