¿Cuál es la forma más segura de ejecutar código de usuario no confiable en Node.js o Python en mi backend?
Ejecuta el código de usuario no confiable en un entorno de ejecución desechable fuera de tu backend web. Para un servicio público que acepta código arbitrario de Node.js o Python, una microVM bien configurada por trabajo o sesión de cliente es una buena opción predeterminada. Mantén fuera las credenciales de producción, bloquea el acceso innecesario a la red y aplica límites de recursos y un tiempo de expiración desde fuera de la carga de trabajo.
Usa tu aplicación de Express, FastAPI o Django para autenticar usuarios, poner trabajos en cola y devolver resultados. Deja que un proceso de trabajo separado gestione la ejecución. Las restricciones a nivel de lenguaje por sí solas no brindan el aislamiento que necesita este diseño.
La configuración adecuada depende de lo que puedan enviar los usuarios. Una calculadora puede aceptar un lenguaje de expresiones sencillo. Un agente de programación que instala paquetes y ejecuta comandos de shell necesita un entorno de ejecución completo. Nos centramos en ese segundo caso, incluido el código que genera una IA a partir de las entradas del usuario.
Elegir primero el límite de aislamiento
| Enfoque | Qué separa | Idoneidad para código arbitrario de usuario |
|---|---|---|
node:vm, eval o exec de Python | Contextos de lenguaje o ámbitos de ejecución | Insuficiente como límite de seguridad |
| Un proceso secundario | Memoria y ciclo de vida del proceso | Aún necesita controles del sistema operativo, sistema de archivos, red y recursos |
| Docker estándar | Espacios de nombres, vistas del sistema de archivos y grupos de recursos | Controles útiles, pero los contenedores comparten el kernel del host de Docker |
| gVisor | Un kernel de aplicación gestiona las llamadas al sistema antes de que lleguen al host | Una opción cuando la compatibilidad y el rendimiento se ajustan a la carga de trabajo |
| Una microVM | Un kernel invitado y un límite de máquina virtual | Una buena opción predeterminada para código hostil, que aún requiere aplicar parches al host y al sistema invitado |
La seguridad de Docker depende del host, el demonio y la configuración. gVisor agrega un kernel de aplicación en el espacio de usuario. Firecracker usa KVM para ejecutar microVMs. Ninguna de estas opciones decide qué credenciales o destinos de red debería exponer tu aplicación. Seguridad de Docker, arquitectura de gVisor, Firecracker.
Para requisitos más estrictos, considera también hosts o cuentas dedicados a los procesos de trabajo. El límite de una VM reduce la exposición que implica compartir un kernel; no demuestra que sea imposible cualquier escape o canal lateral de hardware.
Por qué Node.js y Python necesitan un sandbox externo
Node.js: un contexto de VM no es un límite de seguridad
La propia documentación de Node indica que node:vm no debe servir como mecanismo de seguridad para código no confiable. Pasar un objeto vacío o un tiempo de espera a vm.runInNewContext no cambia esa condición. Documentación de VM de Node.js.
Los hilos de trabajo ayudan con la concurrencia. Un proceso secundario puede proteger al servidor web de una caída común del intérprete. Ninguno elimina automáticamente el acceso al sistema de archivos, las variables de entorno heredadas ni el acceso a la red. Un proceso secundario con los permisos de sistema operativo de tu servidor aún puede acceder a los recursos que esos permisos permitan.
El modelo de permisos de Node también indica que no ofrece garantías de seguridad frente al código malicioso. Úsalo como una restricción adicional donde resulte útil, con un límite externo para los envíos hostiles. Permisos de Node.js.
Python: restringir nombres no contiene un proceso
Python advierte que exec y eval ejecutan código arbitrario. Eliminar __builtins__ o bloquear unas cuantas importaciones no convierte Python sin restricciones en un servicio de ejecución seguro. RestrictedPython también se describe como un subconjunto del lenguaje, sin afirmar que proporcione un sandbox por sí solo. Funciones integradas de Python, RestrictedPython.
Para los datos, acepta un documento JSON de tamaño limitado y valida su estructura. ast.literal_eval no ejecuta código arbitrario de Python, pero Python documenta riesgos de agotamiento de memoria, CPU y pila con entradas hostiles. Tampoco admite programas de propósito general. Documentación de AST de Python.
Para ejecutar Python completo, inicia un intérprete separado dentro del sandbox elegido. Mantén fuera el análisis de las solicitudes web y la autenticación de usuarios.
Una arquitectura de backend para ejecutar código no confiable
El backend debe controlar el registro del trabajo y las reglas de acceso:
- Autentica a quien realiza la llamada y comprueba su cuota. Limita los bytes de la solicitud antes de almacenar el código fuente en un búfer.
- Guarda el código fuente como datos. Asigna el trabajo a un cliente y elige un entorno de ejecución de una lista fija de permitidos.
- Pon el trabajo en cola. Un proceso de trabajo de confianza crea un sandbox nuevo con límites explícitos y un tiempo de expiración.
- Transfiere solo la entrada del trabajo. Ejecuta un comando fijo del intérprete y pasa el código fuente mediante un archivo o stdin.
- Recoge la salida con un límite de tamaño y el estado. Valida los resultados antes de devolverlos o usarlos en otro servicio.
- Detén y elimina el entorno de ejecución. Un proceso de limpieza separado se encarga de los trabajos abandonados.
Mantén la clave del proveedor de sandboxes, el socket de Docker y las credenciales de control de la nube en el proceso de trabajo de confianza. El código de usuario no debe recibir las credenciales que crean o gestionan otros sandboxes.
Comprueba a qué cliente pertenece el recurso en cada operación: ejecutar, cancelar, descargar, reanudar y eliminar. Un ID de sandbox que aporta el cliente es un identificador, no una prueba de acceso. Para conocer el flujo de ejecución completo, consulta nuestra guía de sandboxes para IA.
Controles que debes establecer antes del primer trabajo de usuario
| Control | Política inicial para un ejecutor de código pequeño |
|---|---|
| CPU y RAM | Asignación fija; rechaza las opciones de recursos que envíe el usuario |
| Tiempo transcurrido | Un plazo externo corto y una vida útil máxima del sandbox |
| Procesos y archivos | Límites de cantidad de procesos, archivos abiertos y almacenamiento con permisos de escritura |
| Entrada y salida | Por ejemplo, 32 KiB de código fuente y 64 KiB de stdout/stderr combinados; elige los límites para tu producto |
| Sistema de archivos | Imagen base de solo lectura y un pequeño directorio temporal; sin montajes del directorio de inicio del host ni de la aplicación |
| Credenciales | Sin secretos de producción; usa una autorización de alcance limitado y corta duración solo cuando haga falta |
| Red | Deniega el acceso saliente por defecto; permite explícitamente los destinos necesarios |
| Estado del cliente | Espacio de trabajo nuevo por trabajo, o una sesión vinculada a un solo cliente |
| Uso de la cuenta | Límites por usuario de concurrencia, frecuencia de envío y gasto |
La asignación de CPU limita la velocidad a la que se ejecuta un bucle que ocupa la CPU. Un plazo de tiempo real limita cuánto dura. Aplica ambos. Docker documenta que los contenedores no tienen límites de CPU ni de memoria a menos que los configures. Límites de recursos de Docker.
La política de red debe cubrir los servicios privados y los metadatos de la nube, además de la internet pública. Si los trabajos necesitan HTTP, aplica una lista de permitidos en un proxy de salida o una capa de red que el sistema invitado no pueda cambiar. Comprueba la resolución DNS, los destinos IPv4/IPv6 y las redirecciones. Una función que envuelve fetch dentro del sistema invitado no puede controlar todos los programas que este puede iniciar. Prevención de SSRF de OWASP.
Ejemplos probados de inicio de Docker para Node.js y Python
Estos comandos muestran controles de inicio de contenedores con programas de ejemplo fijos. No incluyen el supervisor externo de tiempo transcurrido y salida que necesitan los envíos públicos. Ejecútalos en un proceso de trabajo separado de tus servidores de aplicaciones. Para un servicio público que ejecuta código hostil, coloca ese proceso dentro del límite de VM elegido o usa un entorno de ejecución de sandbox adecuado.
Usa una imagen de confianza con los parches aplicados. Las etiquetas siguientes resultan prácticas para el ejemplo; fija el resumen criptográfico de la imagen revisada en el despliegue y actualízala mediante tu proceso habitual de parches. Descarga las imágenes antes de aceptar trabajos.
Ejemplo de Node.js
docker run --rm -i --init \
--network none --read-only \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=16m \
--user 65534:65534 --cap-drop ALL \
--security-opt no-new-privileges \
--memory 128m --memory-swap 128m --cpus 0.5 \
--pids-limit 32 --ulimit nofile=64:64 \
--log-driver none \
node:24-bookworm-slim node - <<'JS'
console.log(JSON.stringify({answer: 6 * 7}));
JSLa salida esperada es {"answer":42}. El comando se ejecuta con el UID 65534, elimina las capacidades de Linux, mantiene el sistema de archivos raíz en modo de solo lectura y permite un pequeño /tmp con permisos de escritura. No proporciona ninguna interfaz de red externa ni monta directorios del host.
Ejemplo de Python
docker run --rm -i --init \
--network none --read-only \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=16m \
--user 65534:65534 --cap-drop ALL \
--security-opt no-new-privileges \
--memory 128m --memory-swap 128m --cpus 0.5 \
--pids-limit 32 --ulimit nofile=64:64 \
--log-driver none \
python:3.13-slim-bookworm python -I -B -u - <<'PYCODE'
import json
print(json.dumps({"answer": 6 * 7}))
PYCODELa salida esperada es {"answer": 42}. La opción -I de Python reduce las interferencias de las rutas de importación y del entorno del intérprete. No impide que el programa use los recursos del sistema operativo que permite su contenedor. Opciones de línea de comandos de Python.
Mantén el perfil seccomp predeterminado de Docker. no-new-privileges bloquea la obtención de privilegios mediante mecanismos como los ejecutables setuid. El modo --network none deja solo la interfaz de bucle local. Estos controles cumplen funciones distintas; ninguno sustituye a los demás. Opciones de Docker run, seccomp, aislamiento de red.
La opción noexec del directorio temporal impide la ejecución directa desde ese montaje, pero un intérprete aún puede leer y ejecutar un script guardado allí. No prohíbe los programas de usuario. El límite de 128 MiB también se ajusta a estos pequeños ejemplos, no a todos los paquetes de npm ni a todas las cargas de trabajo de ciencia de datos.
Conectar esto a un backend sin interpolación de shell
En el proceso de trabajo de confianza, construye la solicitud de Docker o del proveedor con argumentos fijos. Node puede usar spawn con shell: false; Python puede usar subprocess.Popen con una lista de argumentos. Envía el código fuente recibido por stdin o mediante una ruta de archivo fija. Nunca lo pongas dentro de un comando de shell, como una cadena que se pasa a sh -c. Consulta procesos secundarios de Node y seguridad de subprocess de Python.
Permite que los usuarios elijan entre los nombres de entornos de ejecución aprobados. No dejes que una solicitud proporcione opciones de Docker, imágenes arbitrarias, rutas del host ni nombres de variables de entorno. La instalación de paquetes también ejecuta código; si la admites, ejecútala bajo la misma política de aislamiento y recursos.
Detener la carga de trabajo, limitar la salida y confirmar la limpieza
Un tiempo de espera en torno a una solicitud HTTP o una llamada al SDK puede hacer que tu backend deje de esperar mientras el programa remoto sigue ejecutándose. Del mismo modo, terminar un proceso local de Docker CLI puede dejar su contenedor activo.
El supervisor de confianza necesita:
- Registrar el ID del contenedor o sandbox antes de que comience la ejecución.
- Leer stdout y stderr de forma incremental, con un límite de bytes compartido.
- Cuando se agote el tiempo o se exceda el límite de salida, detener la carga de trabajo real mediante la API del entorno de ejecución.
- Cerrar o vaciar los flujos conectados sin retener datos sin límite.
- Confirmar la finalización, eliminar el estado desechable y registrar cualquier fallo de limpieza.
--log-driver none en los ejemplos impide que Docker conserve los registros del contenedor. Tu backend aún debe limitar los flujos que lee. En Docker, detén el contenedor identificado por su nombre antes de terminar la CLI conectada. Para un servicio de microVMs, usa su operación de finalización documentada y verifica el estado final.
Establece un tiempo de expiración del lado del proveedor cuando esté disponible. Un bloque finally no puede ejecutarse después de que el proceso de trabajo se quede sin energía, así que mantén un proceso de limpieza programado que coteje los registros de trabajos vencidos con los recursos activos. Haz que sea seguro repetir la limpieza. No marques un trabajo huérfano como limpiado correctamente solo porque el cliente se desconectó.
Qué comprobamos
El 31 de octubre de 2026, ejecutamos ambos comandos en Docker Desktop con Engine 28.1.1, kernel de Linux 6.10.14-linuxkit, ARM64, Node.js 24.22.2 y Python 3.13.18.
| Comprobación | Resultado en ambos entornos de ejecución |
|---|---|
| Cálculo fijo | Devolvió 42 |
| Usuario efectivo | UID 65534 |
Escritura en /etc | Bloqueada |
Escritura y lectura en /tmp | Funcionó |
| Socket saliente hacia una dirección de prueba | Falló con ENETUNREACH |
| Marcador sintético de entorno presente solo en el host | Ausente dentro del contenedor |
| Configuración de recursos | 128 MiB de memoria, sin swap, 32 procesos/hilos, media CPU |
| Política de privilegios | NoNewPrivs habilitado |
| Bucle que ocupa la CPU bajo el supervisor de prueba | Se detuvo tras su plazo de dos segundos |
| Exceso de salida bajo el supervisor de prueba | La captura se detuvo en 64 KiB; la carga de trabajo terminó |
| Contenedor nuevo tras un trabajo anterior | El archivo temporal anterior no estaba presente |
Nuestra primera comprobación del límite de salida reveló un error de limpieza: la CLI de Docker conectada quedó esperando en una tubería llena después de que el contenedor se detuviera. Corregimos el supervisor de prueba para que terminara primero la carga de trabajo y luego cerrara el cliente conectado. Por eso el comando de inicio por sí solo no es un ejecutor completo.
Estas comprobaciones cubren la configuración local del contenedor y el comportamiento del supervisor. No prueban escapes del kernel, no demuestran que todas las vías de acceso estén bloqueadas ni prueban el aislamiento de microVMs de Lizard. Repite las comprobaciones en el entorno de ejecución real del proceso de trabajo que despliegues.
Tratar también los resultados y el estado guardado como no confiables
Un sandbox puede contener un programa mientras su salida ataca al siguiente sistema. Escapa los caracteres de stdout antes de mostrarlo. Valida el JSON devuelto. Nunca deserialices un pickle de Python enviado por un usuario ni evalúes una cadena de JavaScript devuelta en el backend. Advertencia de pickle de Python, manejo de salida de OWASP.
Sirve el HTML generado y las vistas previas desde un origen que no comparta las credenciales de tu panel. Para los archivos descargados, controla los nombres, tamaños y tipos; evita seguir enlaces simbólicos creados por el sistema invitado hacia rutas del host. Limita también el trabajo de extracción y descompresión de archivos.
Un estado nuevo impide que un cliente lea los archivos, las cachés de paquetes o los valores en memoria de otro. Las sesiones persistentes necesitan reglas explícitas de propiedad y retención. Un sandbox pausado conserva el estado; no debe convertirse en el entorno limpio del siguiente usuario. Aplica las mismas reglas a las instantáneas y los volúmenes compartidos.
Dónde encaja Lizard
Lizard Sandboxes ejecuta código en microVMs de Firecracker, cada una con su propio kernel. Lizard SDK admite JavaScript/TypeScript y Python, e incluye operaciones de archivos, ejecución de comandos, consultas de estado y controles del ciclo de vida. Tu backend puede gestionar el entorno de ejecución mediante esa API en lugar de operar sus propios hosts de trabajo. Documentación de Lizard Sandboxes.
Mantén la clave de la API de gestión en el backend. Para trabajos con código hostil, evita pasar lizardToken, que habilita una credencial de proyecto dentro del sistema invitado, o adjuntar credenciales de agente que la tarea no necesite. Establece una vida útil explícita y termina los trabajos desechables después de recoger los resultados con un límite de tamaño. Referencia de Lizard SDK.
El aislamiento de Firecracker no implica que el tráfico saliente esté bloqueado. Comprueba que la política de red desplegada haga cumplir las restricciones de destinos que necesitas. La referencia actual de opciones de creación del SDK no documenta una lista de destinos de salida permitidos; no inventes una opción ni supongas que la configuración de un proxy dentro del sistema invitado la hace cumplir. Si el control estricto del tráfico saliente es obligatorio, verifica una configuración que permita imponerlo antes de aceptar esa carga de trabajo.
Empieza por definir los archivos permitidos, el acceso a la red y el presupuesto de recursos del trabajo. Luego prueba un entorno nuevo con esas restricciones. Usa la guía de inicio rápido de Lizard para la API de ejecución y nuestra comparación de precios de sandboxes al estimar los costos.
Construye con IA. Publica con Lizard.
No necesitas un equipo de plataforma para publicar. Todo tu entorno está a un solo comando de Lizard CLI.
- Servicios
- —
- Add-ons
- —
- Despliegues
- —
- Inicios de sandbox
- —