Escaneo de riesgo RCE para aplicaciones Node: detecta código peligroso rápido
Escaneo de riesgo RCE para apps Node para detectar eval, llamadas inseguras a child_process, inyección de plantillas e imports dinámicos riesgosos frecuentemente presentes en código generado por IA.

Cómo se ve RCE en una aplicación Node real
Remote code execution (RCE) significa que un atacante puede hacer que tu servidor ejecute código que no pretendías. No solo leer un archivo o robar un token, sino ejecutar comandos o cargar código de una manera que les dé control. Si tu app es accesible desde Internet, RCE es una de las rutas más rápidas de un pequeño fallo a una toma total.
En aplicaciones Node, RCE suele aparecer cuando entrada no confiable se trata como instrucciones. Un ejemplo clásico es una funcionalidad de “ejecuta una herramienta por mí”: un usuario envía texto, el servidor construye un comando de shell y child_process lo ejecuta. Si el código no controla estrictamente lo permitido, una petición manipulada puede convertir ese comando en otra cosa por completo.
El código generado por IA tiende a incluir atajos arriesgados porque intenta ser útil. Puede añadir eval para “parsear” datos, construir comandos de shell con plantillas de strings, o cargar módulos dinámicamente según un parámetro de la petición. Esos patrones pueden funcionar en una demo, pero son peligrosos en producción.
Un escaneo de riesgos es una forma rápida de localizar código que parece que podría ejecutar entrada. Te dice dónde mirar primero, no si estás 100% seguro. Cada hallazgo necesita aún una revisión humana para confirmar si la entrada de usuario puede realmente llegar a la línea peligrosa.
Los atacantes típicamente alcanzan RCE a través de puntos de entrada cotidianos:
- Peticiones HTTP (query, body, headers)
- Cargas de archivos (nombres, rutas, contenidos)
- Webhooks ("eventos" de terceros en los que confías demasiado)
- Paneles de administración (menos escrutinio, acciones poderosas)
- Trabajos en segundo plano (mensajes de colas o entradas cron)
Dónde entra la entrada no confiable en tu código
La mayoría de los bugs de RCE empiezan igual: tu app trata los datos de otra persona como si fueran seguros. Al escanear riesgos RCE en una app Node, empieza por mapear cada lugar donde puede entrar datos, incluso si "normalmente" vienen de tu propio equipo.
Los puntos de entrada obvios son las peticiones HTTP. Todo lo que un usuario pueda cambiar es no confiable: query strings, params de ruta, bodies JSON, formularios, headers (especialmente los usados para feature flags o modos debug), cookies, datos de sesión y archivos subidos (incluyendo nombres de archivo).
No toda la entrada llega por la web. Prototipos rápidos suelen incluir scripts auxiliares y funcionalidades admin que evitan las comprobaciones normales y luego se conectan a producción. Presta atención a trabajos en segundo plano y tareas cron que leen de la base de datos, payloads de webhooks de terceros, paneles admin y endpoints “solo internos”, y scripts CLI que aceptan argumentos o leen variables de entorno.
Las “herramientas internas” siguen importando. Los tokens se roban, las VPNs se usan mal y una sola cookie admin filtrada puede convertir un endpoint interno en accesible desde Internet.
Un modelo mental útil es:
input -> parsing -> ejecución
Parsing es donde cambian los tipos (string a JSON, JSON a objeto, objeto a plantilla). Ejecución es donde el peligro se dispara (construir comandos, evaluar código, cargar módulos). Un endpoint solo de soporte que acepta JSON como { "report": "weekly" } puede convertirse en RCE más tarde si alguien añade child_process o un paso de renderizado de plantillas que use el mismo campo.
Un plan sencillo para escanear riesgos RCE
No necesitas herramientas sofisticadas para que un escaneo de riesgos RCE sea útil. Empieza por encontrar las formas de código que más a menudo llevan a “ejecutar lo que el usuario envió”, especialmente en código generado por IA donde los atajos inseguros se cuelan.
Comienza con una búsqueda por palabras clave para crear una lista corta de archivos a revisar:
rg -n \"\\\\beval\\\\b|new Function|child_process|exec\\\\(|execSync\\\\(|spawn\\\\(|spawnSync\\\\(|fork\\\\(|vm\\\\.run|ejs|pug|handlebars|nunjucks|import\\\\(|require\\\\(\\\" .
Luego convierte los aciertos en un mapa de flujo de entrada. Para cada coincidencia, responde dos preguntas:
- ¿Qué datos pueden alcanzar esta línea?
- ¿Quién controla esos datos?
Haz seguimiento de fuentes comunes como parámetros de petición, headers, cookies, payloads de webhooks y cualquier cosa leída de la base de datos que originalmente viniera de usuarios.
Para la priorización, céntrate en la combinación arriesgada de alcanzabilidad y poder: rutas accesibles desde Internet (incluyendo webhooks), cualquier uso de ejecución de comandos del SO o carga de código (child_process, vm, import/require dinámicos), y lugares donde las strings se construyen desde entrada sin una allowlist. Confirma lo que está realmente activo en producción, porque el código muerto y los scripts solo de desarrollo son menor prioridad que el código alcanzado por peticiones reales.
Encuentra eval y Function que puedan ejecutar entrada
Empieza buscando lugares donde el código se construye a partir de cadenas. Los sospechosos habituales son eval() y new Function(). Convierten un valor de texto en algo que el servidor ejecuta. Si un atacante puede influir ese texto, potencialmente puede ejecutar su propio código.
Marca patrones como:
eval(userInput)oeval(someVar)new Function("return " + expr)()setTimeout("doThing(" + x + ")", 0)ysetInterval("...", 1000)- “evaluadores de expresiones” que concatenan cadenas antes de ejecutarlas
“Solo evalúa expresiones pequeñas” sigue siendo peligroso. Una calculadora que parece inofensiva puede volverse en acceso a process.env, lecturas de archivos, llamadas de red o peor si la cadena puede ser modelada por un atacante.
Además, la entrada no tiene que venir directamente del body de una petición. A menudo se cuela a través de archivos de configuración, campos de la base de datos, contenido de un CMS, flags de características o plantillas que almacenan reglas editables por el usuario. En prototipos rápidos a veces encontrarás un motor de reglas rápido como eval(dbRow.rule) que funcionó en una demo y luego se desplegó.
Las sustituciones más seguras dependen de lo que intentas hacer. En general, prefiere un conjunto fijo de operaciones mapeadas a funciones reales, o almacena reglas como datos (por ejemplo JSON) e interprétalas con validación estricta. Si realmente necesitas un lenguaje de expresiones, usa un parser real y solo evalúa nodos soportados.
Revisa child_process por inyección de comandos
Si tu app usa child_process de Node para ejecutar comandos del sistema, trátalo como un riesgo RCE prioritario. El peligro no es “usar un shell” por sí mismo. El peligro es permitir que texto controlado por el usuario forme parte del comando.
Las llamadas de mayor riesgo son exec y execSync, y spawn cuando shell: true está activado. Son fáciles de usar mal porque aceptan una sola cadena de comando. Una vez construyes esa cadena con + o plantillas, un usuario puede colar operadores extras (como ;, &&, |) y ejecutar algo que no pretendías.
Un desliz común en el mundo real es un endpoint “convertir archivo” que hace exec(convert ${req.body.path} -resize 200x200 out.png). Si path contiene image.png; cat /etc/passwd, el shell lo interpreta como dos comandos.
Un patrón más seguro es evitar el shell y pasar un array de argumentos. Por ejemplo: spawn('convert', [inputPath, '-resize', '200x200', outPath], { shell: false }). Aun así debes validar inputPath, pero eliminas la mayoría de trucos de parsing del shell.
Al escanear, busca señales de alarma como cadenas de comando construidas desde campos de petición, shell: true, unión manual de argumentos (args.join(' ')) y helpers de “escape” que sólo reemplazan pocos caracteres.
Si añades logging para ayudar en la priorización, registra lo que importa (con cuidado): el nombre del comando, el array de args (o la cadena completa si debes), y exactamente de dónde vino la entrada (ruta y nombre de campo). No vuelques secretos en los logs.
Detecta rutas de inyección de plantillas del lado servidor
Server-side template injection (SSTI) ocurre cuando tu app construye una plantilla a partir de entrada de usuario y la renderiza en el servidor. Si el motor de plantillas puede ejecutar expresiones, un atacante puede convertir una función de “mensaje personalizado” en ejecución de código.
Un ejemplo sencillo: permites que los usuarios guarden una plantilla de email y luego compilas o renderizas lo que escribieron. Si el motor soporta {{ someExpression }} o llamadas a helpers, el usuario podría leer secretos, llamar funciones o encadenar APIs peligrosas.
En un escaneo rápido, busca lugares donde strings controladas por el usuario se convierten en plantillas, parciales, layouts o nombres de helpers. Patrones comunes incluyen compilar o renderizar entrada de usuario directamente, pasar datos de la petición como locals sin allowlist (por ejemplo res.render("view", req.body)), permitir que los usuarios elijan un parcial/layout por nombre y concatenar rutas, registrar helpers dinámicamente desde datos de la petición, o renderizar "markdown"/"handlebars"/"ejs" almacenados desde un campo de formulario sin una capa de seguridad.
Mitigaciones que suelen funcionar:
- No compilar plantillas a partir de texto crudo de usuarios. Almacena contenido, pero renderízalo como texto plano o un subconjunto de marcado seguro.
- Mantén un mapa estricto de variables para plantillas. No pases objetos enteros como
req,resoprocess. - Trata los nombres de plantillas como rutas de archivo: allowlista plantillas conocidas y rechaza todo lo demás.
Revisa imports dinámicos y carga de módulos riesgosos
La carga dinámica de módulos es conveniente, pero también es una forma común por la que RCE se cuela en apps Node, especialmente cuando el código fue generado rápidamente. Si cualquier parte del nombre del módulo o ruta de archivo viene del usuario, trátalo como entrada no confiable.
Los patrones más peligrosos parecen inocuos al principio: import(userInput), require(userInput) o construir una ruta como require('./plugins/' + name). La gente suele asumir “solo carga archivos locales”, pero los atacantes pueden abusar de trucos de rutas, ubicaciones inesperadas de archivos o alcanzar código que no pretendías exponer.
Escanea por:
import(something)dondesomethingno es un literal stringrequire(something)dondesomethingse construye desde variablesrequire(path.join(base, userValue))y cualquier riesgo de traversal como../- funciones “plugin” que cargan módulos por nombre
- leer un nombre de archivo desde una petición, base de datos o configuración y luego cargarlo
Si realmente necesitas carga dinámica, hazla explícita. Usa una allowlist hardcodeada que mapee nombres a rutas exactas, rechaza todo lo que no esté en el mapa y nunca pases la entrada cruda a require() o import().
No pases por alto los problemas de soporte que habilitan RCE
Aunque encuentres un bug de ejecución obvio, los atacantes normalmente necesitan un poco de ayuda para convertirlo en un incidente real. Un buen escaneo incluye los problemas de soporte que hacen la explotación fácil y la limpieza difícil.
Empieza por los secretos. Los prototipos rápidos a menudo dejan API keys, secretos JWT, URLs de bases de datos y tokens de la nube en texto plano, archivos env de ejemplo o logs. Si un atacante obtiene cualquier ejecución de código, los secretos expuestos les permiten saltar a tus datos y otros sistemas rápidamente.
Los permisos importan igual. Si la app corre como un usuario que puede leer y escribir demasiado, un RCE se convierte en “modificar el servidor”. Vigila permisos amplios de archivos, directorios de la app con permiso de escritura y roles de nube demasiado permisivos.
También revisa configuraciones de producción que abren puertas silenciosamente: modo desarrollo en producción, flags de debug activadas (errores verbosos, stack traces, hot reload), rutas admin ocultas, CORS inseguro y directorios de uploads o tmp que sean escribibles y estén expuestos.
Finalmente, las dependencias pueden ser el eslabón más débil. Paquetes desactualizados con CVE conocidas de RCE pueden convertir una ruta inofensiva en una comprometedora. Una revisión rápida de tu lockfile y advisories forma parte del trabajo.
Errores comunes al intentar arreglar riesgos RCE
La forma más rápida de perder tiempo con RCE es arreglar lo visible sin probar cómo se alcanza el código. Un helper limpio puede seguir siendo peligroso si está detrás de una ruta, middleware, job cron o webhook que acepta entrada externa.
Una trampa común es asumir “no es alcanzable” porque no ves un botón en la UI. Traza la ruta de todos modos: request -> router -> middleware -> controller -> helper. Haz lo mismo para workers y manejadores de webhooks. Muchos incidentes reales empiezan en paths que la gente rara vez prueba, como un webhook de pagos, un callback de OAuth o una cola de trabajos interna.
Otro error es tratar el síntoma en vez de la causa. Escapar output o añadir codificación no hace seguro a eval, Function, exec o spawn si entrada no confiable puede alcanzarlos. Si tu escaneo encuentra ejecución dinámica, el objetivo suele ser eliminarla, no “sanearla”.
Los “sanitizadores” basados en regex son otro callejón sin salida. Si una solución depende de “bloquear estos caracteres”, asume que alguien lo bypassará con espacios, comillas, metacaracteres del shell o trucos de codificación.
Hábitos que previenen hallazgos repetidos:
- Comprueba la alcanzabilidad probando rutas, middleware, workers y webhooks
- Reemplaza ejecución dinámica por comandos fijos, plantillas fijas o librerías validadas
- Valida entrada por tipo e intención (IDs, enums, esquemas estrictos), no por regex adivinos
- Añade tests para payloads maliciosos para que el problema no vuelva
Lista de verificación rápida antes de lanzar
Usa esto tras tu escaneo y otra vez antes del release. El objetivo es simple: nada en tu servidor debería poder convertir entrada de usuario en código, un comando de shell o una ruta de módulo.
- No
eval,new FunctionnisetTimeout/setIntervalbasados en strings en código servidor. - No
execniexecSync. Paraspawn/execFile, pasa args como array y no habilitesshell: true. - No compilar plantillas servidor-side a partir de texto proporcionado por usuarios.
- Cualquier import/carga dinámica está allowlisteada y no acepta rutas construidas por usuarios.
- La validación de entrada ocurre en los límites (handlers HTTP, consumidores de colas, webhooks), con reglas claras por campo.
Una prueba de cordura rápida: imagina que un atacante controla un query param, un header y un campo JSON. ¿Alguno de esos valores puede alcanzar un ejecutor de código (eval), un lanzador de comandos (child_process), un compilador de plantillas o una ruta de import sin ser rechazado?
Ejemplo: una pequeña función que accidentalmente se convierte en RCE
Un lío común en el mundo real es una página admin interna construida por IA con un cuadro “Run maintenance”. La idea es inocua: escribir reindex-users o clear-cache, clicar Run y que el servidor lo ejecute.
El primer problema es cómo se cablea la funcionalidad. El endpoint suele tomar req.body.command y pasarlo a child_process.exec(), o construye una ruta de script desde ello y hace import() del archivo. Si un atacante puede alcanzar ese endpoint (autenticación débil, cookie admin filtrada, falta de chequeo de roles), puede probar entradas como reindex-users && cat .env o ../../../../tmp/payload y tienes remote code execution.
Un escaneo típicamente marcará rápido: child_process.exec() alimentado por datos de la petición (incluso tras “sanitizar”), imports dinámicos o require() construidos desde strings, renderizado de plantillas que compila entrada de usuario y helpers como eval() o new Function() dejados por generadores.
Tras el escaneo, confirma los detalles manualmente: ¿la ruta es alcanzable sin un check admin estricto?, ¿las entradas están realmente controladas?, ¿qué se ejecuta en el servidor (shell, node o un runner de scripts)? El código peligroso suele estar escondido detrás de feature flags “temporales”.
Lo primero que arreglas es lo que elimina las primitivas de ejecución:
- Elimina el cuadro de comando de texto libre y reemplázalo por una allowlist de acciones nombradas.
- Sustituye
execpor APIs más seguras (o realiza el trabajo en proceso con funciones normales). - Bloquea la ruta a un rol admin real y registra cada acción.
- Ejecuta el servicio con los mínimos privilegios y elimina secretos del entorno de ejecución.
El resultado final es simple: menos caminos para alcanzar el código y ninguna forma directa de ejecutar entrada.
Pasos siguientes: convertir hallazgos en código seguro y listo para producción
Trata cada hallazgo como una decisión de producto, no solo como un bug. Algunos patrones arriesgados merecen parchearse, pero otros es mejor eliminarlos porque volverán a aparecer con futuros cambios.
Una manera práctica de decidir es etiquetar cada problema:
- Patch: puedes hacer que el enfoque actual sea seguro con validación estricta, allowlists y APIs más seguras.
- Refactor: la funcionalidad es necesaria, pero el diseño invita a errores.
- Remove/replace: la funcionalidad existe mayormente porque un generador la añadió.
Tras las correcciones, escribe una nota de seguridad corta en tu repo. Manténla clara y específica: nada de eval/Function con datos de petición, no strings controladas por usuarios en child_process, las plantillas no deben compilar entrada de usuario, los imports dinámicos deben venir de una allowlist. Ayuda a tu yo futuro y acelera las revisiones de código.
Añade guardarraíles ligeros para que los mismos problemas no vuelvan. Una búsqueda guardada o una checklist de revisión para eval, new Function, child_process.exec, compilación de plantillas e import() dinámico desde variables detecta mucho.
Si heredaste un prototipo generado por herramientas como Lovable, Bolt, v0, Cursor o Replit y quieres una segunda mirada rápida, FixMyMess (fixmymess.ai) se enfoca en diagnosticar y reparar patrones riesgosos como estos, y luego verificar las correcciones con revisión humana experta para que la app aguante en producción.
Preguntas Frecuentes
What exactly counts as RCE in a Node app?
RCE (remote code execution) es cuando alguien puede hacer que tu servidor ejecute código o comandos que no pretendías. Suele ser más grave que una fuga de datos porque puede llevar al control total de la app, la máquina donde corre y cualquier secreto al que la app tenga acceso.
What’s the fastest way to do an RCE risk scan without fancy tools?
Empieza identificando dónde entra datos no confiables: peticiones HTTP, webhooks, cargas de archivos, herramientas de administración y trabajos en segundo plano. Luego busca “primitivas de ejecución” como eval, new Function, child_process, compilación de plantillas y import()/require() dinámicos, y traza si datos controlados por el usuario pueden alcanzarlos.
Will a keyword scan tell me if I’m definitely vulnerable?
No. Un escaneo tipo grep encuentra formas de código sospechosas, no exploits confirmados. Aún necesitas trazar el flujo de datos y confirmar la alcanzabilidad en producción, porque algunos hallazgos son código muerto o sólo se ejecutan con entradas de confianza.
Why are internal admin endpoints such a common RCE problem?
Incluso los endpoints “internos” pueden volverse accesibles si se roban tokens, se filtran cookies o hay una mala configuración que expone la ruta. Trata las herramientas internas como superficies de ataque reales: autenticación estricta, validación de entrada estricta y nada de ejecución libre de texto.
Is `eval()` always a security bug, or can it be safe?
Es peligroso siempre que la cadena evaluada pueda ser influida por usuarios, aunque sea indirectamente a través de campos de base de datos, configuración, contenido de un CMS o flags de características. La mayoría de las soluciones seguras eliminan la evaluación de cadenas por completo y la reemplazan por una lista pequeña de operaciones permitidas implementadas como funciones reales.
What’s the real risk with `child_process.exec()` and command strings?
exec y execSync son de alto riesgo porque ejecutan una cadena de comando en un shell, lo que facilita inyectar operadores extras. Prefiere ejecución sin shell con arreglos de argumentos (como spawn o execFile), y aun así valida entradas como nombres de archivo e IDs para que los atacantes no apunten la herramienta a archivos inesperados.
How do I know if my template engine usage can become RCE?
SSTI ocurre cuando compilas o renderizas una plantilla construida a partir de entrada de usuario y el motor de plantillas soporta expresiones o helpers. Por defecto, guarda el texto del usuario como contenido y renderízalo como texto plano (o un subconjunto seguro de marcado) en lugar de tratarlo como plantilla del lado del servidor.
Why are dynamic `import()` or `require()` patterns considered RCE risks?
Es arriesgado cuando cualquier parte del nombre del módulo o ruta viene de datos controlados por el usuario, aunque “debería” cargar sólo archivos locales. La solución práctica es mapear nombres permitidos a módulos exactos en el código y rechazar cualquier cosa que no esté en esa allowlist, en lugar de pasar la entrada cruda a require() o import().
Can I just “sanitize” input to make RCE issues go away?
No de forma fiable. Bloquear caracteres como ; o && es fácil de eludir con trucos de codificación, espacios o reglas de parsing del shell, y no resuelve la inyección en plantillas ni la ejecución dinámica de código. La manera más segura es eliminar la primitiva de ejecución o limitarla a acciones fijas y allowlisteadas.
What should I fix first if a scan finds multiple risky spots?
Prioriza lo que sea alcanzable desde Internet (incluyendo webhooks) que pueda ejecutar código, correr comandos del sistema, compilar plantillas o cargar módulos dinámicamente. Si heredaste código generado por IA y necesitas una revisión rápida y práctica, equipos como FixMyMess se enfocan en diagnosticar estos patrones, repararlos y verificar las correcciones con revisión humana para que la app sea segura para producción.