Skip to content

Mejores Prácticas de Seguridad

Intención

Este documento pretende ampliar el actual modelo de amenazas y proporcionar directrices exhaustivas sobre cómo asegurar una aplicación Node.js.

Contenido del Documento

  • Mejores prácticas: Una forma simplificada y concisa de ver las mejores prácticas. Podemos usar este issue o esta guía como punto de partida. Es importante tener en cuenta que este documento es específico de Node.js; si busca algo más amplio, considere las Mejores Prácticas OSSF.
  • Ataques explicados: ilustrar y documentar en lenguaje sencillo, con algunos ejemplos de código (si es posible), los ataques que se mencionan en el modelo de amenazas.
  • Bibliotecas de Terceros: definir amenazas (ataques de typosquatting, paquetes maliciosos...) y mejores prácticas con respecto a las dependencias de los módulos node, etc...

Lista de Amenazas

Denegación de Servicio del servidor HTTP (CWE-400)

Este es un ataque en el que la aplicación deja de estar disponible para el propósito para el que fue diseñada debido a la forma en que procesa las solicitudes HTTP entrantes. Estas solicitudes no tienen que ser deliberadamente creadas por un actor malicioso: un cliente mal configurado o con errores también puede enviar un patrón de solicitudes al servidor que resulten en una denegación de servicio.

Las solicitudes HTTP son recibidas por el servidor HTTP de Node.js y entregadas al código de la aplicación a través del controlador de solicitudes registrado. El servidor no analiza el contenido del cuerpo de la solicitud. Por lo tanto, cualquier denegación de servicio causada por el contenido del cuerpo después de que se entrega al controlador de solicitudes no es una vulnerabilidad en Node.js en sí mismo, ya que es responsabilidad del código de la aplicación manejarlo correctamente.

Asegúrese de que el servidor web maneje correctamente los errores de socket; por ejemplo, cuando se crea un servidor sin un controlador de errores, será vulnerable a la denegación de servicio.

javascript
import net from 'node:net'
const server = net.createServer(socket => {
  // socket.on('error', console.error) // esto evita que el servidor se bloquee
  socket.write('Echo server\r\n')
  socket.pipe(socket)
})
server.listen(5000, '0.0.0.0')

Si se realiza una solicitud incorrecta, el servidor podría bloquearse.

Un ejemplo de un ataque de denegación de servicio que no es causado por el contenido de la solicitud es Slowloris. En este ataque, las solicitudes HTTP se envían lentamente y fragmentadas, un fragmento a la vez. Hasta que se entrega la solicitud completa, el servidor mantendrá los recursos dedicados a la solicitud en curso. Si se envían suficientes de estas solicitudes al mismo tiempo, la cantidad de conexiones concurrentes pronto alcanzará su máximo, lo que resultará en una denegación de servicio. Así es como el ataque depende no del contenido de la solicitud, sino de la sincronización y el patrón de las solicitudes que se envían al servidor.

Mitigaciones

  • Usar un proxy inverso para recibir y reenviar solicitudes a la aplicación Node.js. Los proxies inversos pueden proporcionar almacenamiento en caché, equilibrio de carga, listas negras de IP, etc., lo que reduce la probabilidad de que un ataque DoS sea efectivo.
  • Configurar correctamente los tiempos de espera del servidor, de modo que se puedan descartar las conexiones que están inactivas o en las que las solicitudes llegan demasiado lentamente. Consulte los diferentes tiempos de espera en http.Server, particularmente headersTimeout, requestTimeout, timeout y keepAliveTimeout.
  • Limitar el número de sockets abiertos por host y en total. Consulte la documentación de http, en particular agent.maxSockets, agent.maxTotalSockets, agent.maxFreeSockets y server.maxRequestsPerSocket.

Reenlazado DNS (CWE-346)

Este es un ataque que puede dirigirse a aplicaciones Node.js que se ejecutan con el inspector de depuración habilitado usando el interruptor --inspect.

Dado que los sitios web abiertos en un navegador web pueden realizar solicitudes WebSocket y HTTP, pueden dirigirse al inspector de depuración que se ejecuta localmente. Esto generalmente se evita mediante la política del mismo origen implementada por los navegadores modernos, que prohíbe a los scripts acceder a recursos de orígenes diferentes (lo que significa que un sitio web malicioso no puede leer datos solicitados desde una dirección IP local).

Sin embargo, a través del reenlazado DNS, un atacante puede controlar temporalmente el origen de sus solicitudes para que parezcan originarse de una dirección IP local. Esto se hace controlando tanto un sitio web como el servidor DNS utilizado para resolver su dirección IP. Consulte la wiki de Reenlazado DNS para obtener más detalles.

Mitigaciones

  • Deshabilitar el inspector en la señal SIGUSR1 adjuntando un escuchador process.on(‘SIGUSR1’, …) a él.
  • No ejecutar el protocolo del inspector en producción.

Exposición de información sensible a un actor no autorizado (CWE-552)

Todos los archivos y carpetas incluidos en el directorio actual se envían al registro npm durante la publicación del paquete.

Existen algunos mecanismos para controlar este comportamiento mediante la definición de una lista negra con .npmignore y .gitignore o mediante la definición de una lista blanca en package.json

Mitigaciones

  • Usar npm publish --dry-run para listar todos los archivos que se publicarán. Asegúrate de revisar el contenido antes de publicar el paquete.
  • También es importante crear y mantener archivos de ignorancia como .gitignore y .npmignore. En estos archivos, puedes especificar qué archivos/carpetas no deben publicarse. La propiedad files en package.json permite la operación inversa, una lista de permitidos.
  • En caso de exposición, asegúrate de despublicar el paquete.

Contrabandeo de solicitudes HTTP (CWE-444)

Este es un ataque que involucra dos servidores HTTP (usualmente un proxy y una aplicación Node.js). Un cliente envía una solicitud HTTP que pasa primero por el servidor front-end (el proxy) y luego es redirigido al servidor back-end (la aplicación). Cuando el front-end y el back-end interpretan solicitudes HTTP ambiguas de manera diferente, existe la posibilidad de que un atacante envíe un mensaje malicioso que no será visto por el front-end pero sí por el back-end, "contrabandeándolo" efectivamente más allá del servidor proxy.

Consulta CWE-444 para una descripción y ejemplos más detallados.

Dado que este ataque depende de que Node.js interprete las solicitudes HTTP de manera diferente a un servidor HTTP (arbitrario), un ataque exitoso puede deberse a una vulnerabilidad en Node.js, el servidor front-end, o ambos. Si la forma en que Node.js interpreta la solicitud es consistente con la especificación HTTP (ver RFC7230), entonces no se considera una vulnerabilidad en Node.js.

Mitigaciones

  • No uses la opción insecureHTTPParser al crear un servidor HTTP.
  • Configura el servidor front-end para normalizar las solicitudes ambiguas.
  • Monitorea continuamente las nuevas vulnerabilidades de contrabando de solicitudes HTTP tanto en Node.js como en el servidor front-end elegido.
  • Usa HTTP/2 de extremo a extremo y deshabilita la degradación de HTTP si es posible.

Exposición de información a través de ataques de tiempo (CWE-208)

Este es un ataque que permite al atacante aprender información potencialmente sensible midiendo, por ejemplo, cuánto tiempo tarda la aplicación en responder a una solicitud. Este ataque no es específico de Node.js y puede dirigirse a casi todos los tiempos de ejecución.

El ataque es posible siempre que la aplicación utilice un secreto en una operación sensible al tiempo (por ejemplo, una rama). Considere el manejo de la autenticación en una aplicación típica. Aquí, un método de autenticación básico incluye correo electrónico y contraseña como credenciales. La información del usuario se recupera de la entrada proporcionada por el usuario, idealmente de un DBMS. Al recuperar la información del usuario, la contraseña se compara con la información del usuario recuperada de la base de datos. El uso de la comparación de cadenas integrada lleva más tiempo para valores de la misma longitud. Esta comparación, cuando se ejecuta durante una cantidad aceptable, aumenta involuntariamente el tiempo de respuesta de la solicitud. Al comparar los tiempos de respuesta de las solicitudes, un atacante puede adivinar la longitud y el valor de la contraseña en una gran cantidad de solicitudes.

Mitigaciones

  • La API criptográfica expone una función timingSafeEqual para comparar valores sensibles reales y esperados utilizando un algoritmo de tiempo constante.
  • Para la comparación de contraseñas, puede utilizar el scrypt disponible también en el módulo criptográfico nativo.
  • De manera más general, evite usar secretos en operaciones de tiempo variable. Esto incluye ramificarse en secretos y, cuando el atacante podría estar ubicado en la misma infraestructura (por ejemplo, la misma máquina en la nube), usar un secreto como índice en la memoria. Escribir código de tiempo constante en JavaScript es difícil (en parte debido al JIT). Para aplicaciones criptográficas, utilice las API criptográficas integradas o WebAssembly (para algoritmos no implementados de forma nativa).

Módulos de terceros maliciosos (CWE-1357)

Actualmente, en Node.js, cualquier paquete puede acceder a recursos potentes como el acceso a la red. Además, debido a que también tienen acceso al sistema de archivos, pueden enviar cualquier dato a cualquier lugar.

Todo el código que se ejecuta en un proceso de nodo tiene la capacidad de cargar y ejecutar código arbitrario adicional utilizando eval() (o sus equivalentes). Todo el código con acceso de escritura al sistema de archivos puede lograr lo mismo escribiendo en archivos nuevos o existentes que se cargan.

Node.js tiene un mecanismo de política experimental¹ para declarar el recurso cargado como no confiable o confiable. Sin embargo, esta política no está habilitada de forma predeterminada. Asegúrese de fijar las versiones de las dependencias y ejecute comprobaciones automáticas de vulnerabilidades utilizando flujos de trabajo comunes o scripts npm. Antes de instalar un paquete, asegúrese de que este paquete se mantiene e incluye todo el contenido que esperaba. Tenga cuidado, el código fuente de GitHub no siempre es el mismo que el publicado, verifíquelo en node_modules.

Ataques a la cadena de suministro

Un ataque a la cadena de suministro en una aplicación Node.js ocurre cuando una de sus dependencias (directas o transitivas) se ve comprometida. Esto puede ocurrir debido a que la aplicación es demasiado laxa en la especificación de las dependencias (permitiendo actualizaciones no deseadas) y/o errores tipográficos comunes en la especificación (vulnerable a typosquatting).

Un atacante que toma el control de un paquete ascendente puede publicar una nueva versión con código malicioso. Si una aplicación Node.js depende de ese paquete sin ser estricta sobre qué versión es segura de usar, el paquete puede actualizarse automáticamente a la última versión maliciosa, comprometiendo la aplicación.

Las dependencias especificadas en el archivo package.json pueden tener un número de versión exacto o un rango. Sin embargo, al anclar una dependencia a una versión exacta, sus dependencias transitivas no están ancladas. Esto todavía deja la aplicación vulnerable a actualizaciones no deseadas o inesperadas.

Posibles vectores de ataque:

  • Ataques de typosquatting
  • Envenenamiento de lockfile
  • Mantenedores comprometidos
  • Paquetes maliciosos
  • Confusiones de dependencias
Mitigaciones
  • Evitar que npm ejecute scripts arbitrarios con --ignore-scripts
    • Además, puedes deshabilitarlo globalmente con npm config set ignore-scripts true
  • Anclar las versiones de las dependencias a una versión específica inmutable, no a un rango de versiones o una fuente mutable.
  • Usar lockfiles, que anclan cada dependencia (directa y transitiva).
  • Automatizar las comprobaciones de nuevas vulnerabilidades usando CI, con herramientas como npm-audit.
    • Herramientas como Socket se pueden usar para analizar paquetes con análisis estático para encontrar comportamientos riesgosos como el acceso a la red o al sistema de archivos.
  • Usar npm ci en lugar de npm install. Esto refuerza el lockfile para que las inconsistencias entre él y el archivo package.json causen un error (en lugar de ignorar silenciosamente el lockfile en favor de package.json).
  • Revisar cuidadosamente el archivo package.json para detectar errores/errores tipográficos en los nombres de las dependencias.

Violación de acceso a memoria (CWE-284)

Los ataques basados en memoria o en el montón dependen de una combinación de errores de gestión de memoria y un asignador de memoria explotable. Al igual que todos los tiempos de ejecución, Node.js es vulnerable a estos ataques si sus proyectos se ejecutan en una máquina compartida. El uso de un montón seguro es útil para evitar que se filtre información confidencial debido a desbordamientos y subdesbordamientos de punteros.

Desafortunadamente, un montón seguro no está disponible en Windows. Puede encontrar más información en la documentación de montón seguro de Node.js.

Mitigaciones

  • Use --secure-heap=n dependiendo de su aplicación, donde n es el tamaño máximo de bytes asignado.
  • No ejecute su aplicación de producción en una máquina compartida.

Monkey Patching (CWE-349)

Monkey patching se refiere a la modificación de propiedades en tiempo de ejecución con el objetivo de cambiar el comportamiento existente. Ejemplo:

js
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // sobrescribiendo el [].push global
}

Mitigaciones

El indicador --frozen-intrinsics habilita los intrínsecos congelados experimentales¹, lo que significa que todos los objetos y funciones JavaScript integrados están congelados recursivamente. Por lo tanto, el siguiente fragmento no sobrescribirá el comportamiento predeterminado de Array.prototype.push

js
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // sobrescribiendo el [].push global
}
// Sin capturar:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// No se puede asignar a la propiedad de solo lectura 'push' del objeto '

Sin embargo, es importante mencionar que aún puede definir globales nuevos y reemplazar globales existentes usando globalThis

bash
globalThis.foo = 3; foo; // todavía puede definir nuevas variables globales 3
globalThis.Array = 4; Array; // Sin embargo, también puede reemplazar las variables globales existentes 4

Por lo tanto, Object.freeze(globalThis) se puede usar para garantizar que no se reemplacen las variables globales.

Ataques de contaminación de prototipos (CWE-1321)

La contaminación de prototipos se refiere a la posibilidad de modificar o inyectar propiedades en elementos del lenguaje Javascript abusando del uso de __proto__, _constructor, prototype y otras propiedades heredadas de prototipos integrados.

js
const a = { a: 1, b: 2 }
const data = JSON.parse('{"__proto__": { "polluted": true}}')
const c = Object.assign({}, a, data)
console.log(c.polluted) // true
// Posible Denegación de Servicio
const data2 = JSON.parse('{"__proto__": null}')
const d = Object.assign(a, data2)
d.hasOwnProperty('b') // Uncaught TypeError: d.hasOwnProperty no es una función

Esta es una vulnerabilidad potencial heredada del lenguaje JavaScript.

Ejemplos

Mitigaciones

  • Evitar fusiones recursivas inseguras, ver CVE-2018-16487.
  • Implementar validaciones de esquema JSON para solicitudes externas/no confiables.
  • Crear objetos sin prototipo usando Object.create(null).
  • Congelar el prototipo: Object.freeze(MyObject.prototype).
  • Deshabilitar la propiedad Object.prototype.__proto__ usando la bandera --disable-proto.
  • Comprobar que la propiedad existe directamente en el objeto, no en el prototipo usando Object.hasOwn(obj, keyFromObj).
  • Evitar el uso de métodos de Object.prototype.

Elemento de ruta de búsqueda no controlada (CWE-427)

Node.js carga módulos siguiendo el Algoritmo de Resolución de Módulos. Por lo tanto, asume que el directorio en el que se solicita un módulo (require) es de confianza.

Esto significa que se espera el siguiente comportamiento de la aplicación. Suponiendo la siguiente estructura de directorios:

  • app/
    • server.js
    • auth.js
    • auth

Si server.js usa require('./auth'), seguirá el algoritmo de resolución de módulos y cargará auth en lugar de auth.js.

Mitigaciones

El uso del mecanismo experimental¹ de políticas con verificación de integridad puede evitar la amenaza anterior. Para el directorio descrito anteriormente, se puede usar el siguiente policy.json

json
{
  "resources": {
    "./app/auth.js": {
      "integrity": "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8="
    },
    "./app/server.js": {
      "dependencies": {
        "./auth": "./app/auth.js"
      },
      "integrity": "sha256-NPtLCQ0ntPPWgfVEgX46ryTNpdvTWdQPoZO3kHo0bKI="
    }
  }
}

Por lo tanto, al requerir el módulo auth, el sistema validará la integridad y lanzará un error si no coincide con el esperado.

bash
» node --experimental-policy=policy.json app/server.js
node:internal/policy/sri:65
      throw new ERR_SRI_PARSE(str, str[prevIndex], prevIndex);
      ^
SyntaxError [ERR_SRI_PARSE]: Subresource Integrity string "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8=%" had an unexpected "%" at position 51
    at new NodeError (node:internal/errors:393:5)
    at Object.parse (node:internal/policy/sri:65:13)
    at processEntry (node:internal/policy/manifest:581:38)
    at Manifest.assertIntegrity (node:internal/policy/manifest:588:32)
    at Module._compile (node:internal/modules/cjs/loader:1119:21)
    at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
    at Module.load (node:internal/modules/cjs/loader:1037:32)
    at Module._load (node:internal/modules/cjs/loader:878:12)
    at Module.require (node:internal/modules/cjs/loader:1061:19)
    at require (node:internal/modules/cjs/helpers:99:18) {
  code: 'ERR_SRI_PARSE'
}

Nota: siempre se recomienda el uso de --policy-integrity para evitar mutaciones de políticas.

Funciones Experimentales en Producción

No se recomienda el uso de funciones experimentales en producción. Las funciones experimentales pueden sufrir cambios importantes si es necesario, y su funcionalidad no es segura ni estable. Aunque, se agradece mucho la retroalimentación.

Herramientas OpenSSF

OpenSSF lidera varias iniciativas que pueden ser muy útiles, especialmente si planea publicar un paquete npm. Estas iniciativas incluyen:

  • OpenSSF Scorecard Scorecard evalúa proyectos de código abierto utilizando una serie de comprobaciones de riesgo de seguridad automatizadas. Puede usarlo para evaluar proactivamente las vulnerabilidades y dependencias en su base de código y tomar decisiones informadas sobre la aceptación de vulnerabilidades.
  • Programa de Insignias de Mejores Prácticas OpenSSF Los proyectos pueden autocertificarse voluntariamente describiendo cómo cumplen con cada mejor práctica. Esto generará una insignia que se puede agregar al proyecto.