Skip to main content

5 prácticas recomendadas de seguridad para React con TypeScript

Escrito por

Marcelo Oliveira

feature typescript react

8 de diciembre de 2022

0 minutos de lectura

Como biblioteca enfocada en crear interfaces de usuario, en lugar de ser un framework completo, React permite que los desarrolladores elijan las bibliotecas que prefieran para distintos aspectos de una aplicación, como el enrutamiento, el historial y la autenticación. Por su parte, Microsoft creó TypeScript como una extensión de JavaScript para incorporar tipado estático opcional a un lenguaje que, de otro modo, tiene un tipado flexible.

Usar TypeScript con React ofrece varias ventajas al crear aplicaciones, como la posibilidad de simplificar los componentes de React y mejorar la compatibilidad con JavaScript XML (JSX) para la validación de tipos estáticos. Como podemos usar componentes de JavaScript en un proyecto de TypeScript, los equipos de desarrollo con experiencia en JavaScript pueden aprovechar esos conocimientos y beneficiarse de la programación con tipado estricto.

Hay muchos boilerplates para iniciar proyectos de React, entre ellos Create React App, Create Next App, Vite, React Boilerplate y React Starter Kit.

Create React App es una herramienta independiente que se puede ejecutar con npm o Yarn. Una vez instalada, podemos generar y ejecutar un proyecto nuevo con solo unos cuantos comandos.

Primero, abre la terminal y ejecuta el siguiente comando para instalar la herramienta Create React App:

npm install -g create-react-app

Luego, crea un proyecto con la plantilla de TypeScript usando el ejecutor de paquetes de Node (npx):

npx create-react-app [webapp-name] --template typescript

También puedes crear el proyecto con el administrador de paquetes Yarn:

yarn create react-app [webapp-name] --template typescript

los posibles riesgos de seguridad y cómo mitigarlos.

Activa el modo estricto

El modo estricto activa automáticamente los parámetros del compilador de TypeScript relacionados con las reglas de tipos de datos. Cuando está activado, TypeScript valida el código con reglas de tipos estrictas y obliga a los desarrolladores a respetar las limitaciones de los tipos de datos asignados a variables, constantes, parámetros y valores de retorno de funciones. El modo estricto es importante porque permite detectar y corregir errores de incompatibilidad de tipos al inicio del desarrollo.

Supongamos que una aplicación tiene la siguiente función:

export async function deleteComment(slug, commentId): Promise<void> {
  await axios.delete(`articles/${slug}/comments/${commentId}`);
}

Invocamos la función deleteComment pasando el argumento slug como una cadena y commentId como un número:

deleteComment('whats-new-in-react', 1257);

Sin embargo, esto no impide que invoquemos la función con dos cadenas:

deleteComment('foo', 'bar');

Es posible que las reglas de negocio no permitan el código anterior, pero, como no especificamos los tipos, se supone que ambos parámetros son del tipo predeterminado any. Por lo tanto, TypeScript no puede ayudarnos a identificar el problema a menos que activemos el modo estricto.

Las aplicaciones de React recién creadas incluyen el valor strict establecido en true. Sin embargo, este valor puede ser distinto en proyectos existentes.

Para asegurarte de que el proyecto de TypeScript se ejecute en modo estricto, abre el archivo tsconfig.json y comprueba que el valor de la configuración strict sea true:

{
  ...
  "strict": true,
  ...
}

Volvamos a la función deleteComment. Un pequeño cambio genera errores de compilación de TypeScript de inmediato:

Editor de TypeScript que muestra errores por tipos «any» implícitos en los parámetros slug y commentId de conduit.ts

Agrega los tipos string y number a los parámetros de deleteComment:

export async function deleteComment(slug: string, commentId: number): Promise<void> {
  await axios.delete(`articles/${slug}/comments/${commentId}`);
}

Esto genera errores de compilación que impiden que los desarrolladores creen código del cliente que invoque por error la función deleteComment con tipos no válidos, como en el siguiente ejemplo:

deleteComment(null, true);
Editor de TypeScript que muestra errores en conduit.ts: se pasa null a un parámetro de cadena y un valor booleano a un parámetro numérico.

La opción strict activa automáticamente otras opciones recomendadas del compilador relacionadas con la verificación de tipos más estricta.

No uses el tipo de retorno any en callbacks cuyo valor se ignorará

Si declaras el tipo de retorno de un callback como any y luego usas por error su valor de retorno cuando la función no devuelve ningún valor, puedes cometer un error que pasará inadvertido.

Considera una función llamada onListFieldKeyUp que recibe como parámetro un callback llamado onEnter:

export function onListFieldKeyUp(onEnter: () => any): (ev: React.KeyboardEvent) => void {
  return (ev) => {
    if (ev.key === 'Enter') {
      ev.preventDefault();  
      var enterResult = onEnter();
      //do something with enterResult
      }
    }
  };
}

Observa que la variable enterResult almacena el resultado de la función callback onEnter para usarlo más adelante. Aunque el callback no devuelve ningún valor, declaramos la variable enterResult con el tipo any, por lo que TypeScript no puede avisarte de que hay un problema.

¿Cuál es la solución?

Primero, si sabes que la función onEnter no devuelve ningún valor, reemplaza el tipo any del parámetro callback por void. Ahora, TypeScript muestra el error «No se puede comprobar la veracidad de una expresión de tipo 'void'».

Editor de código que muestra FormGroup.tsx con un error de TypeScript: no se puede comprobar si una expresión de tipo 'void' es verdadera.

Por último, elimina el código que almacena el valor de retorno de onEnter en la variable enterResult:

export function onListFieldKeyUp(onEnter: () => void): (ev: React.KeyboardEvent) => void {
  return (ev) => {
    if (ev.key === 'Enter') {
      ev.preventDefault();
      onEnter();
    }
  };
}

Ataques de renderizado del lado del servidor en React

Una aplicación web puede renderizar HTML en el cliente o en el servidor. Los frameworks y las bibliotecas modernas de JavaScript, como React, adoptan el renderizado del lado del servidor. Este enfoque ofrece ventajas de rendimiento, como acelerar la carga de las páginas: permite que el back end procese rápidamente la página completa y envíe el contenido estático de HTML, CSS y JavaScript al front end. Así, los usuarios pueden navegar y ver la página web al instante. Este enfoque también ayuda con la optimización para motores de búsqueda (SEO), ya que las páginas que cargan rápido obtienen mejores puntuaciones en los algoritmos de búsqueda.

El cross-site scripting (XSS) es un tipo de ataque en el que los atacantes inyectan scripts maliciosos del lado del cliente en una página web. React se diseñó para proteger contra XSS. Sin embargo, una programación inadecuada y el renderizado del lado del servidor en React pueden generar una vulnerabilidad XSS que usuarios maliciosos podrían explotar.

Por ejemplo, nunca concatenes datos no sanitizados con el resultado de la función renderToStaticMarkup antes de enviar la cadena al cliente:

app.get("/", function (req, res) {
  return res.send(
    ReactDOMServer.renderToStaticMarkup(
      React.createElement("h1", null, "Hello World!")
    ) + someUnsanitizedData
  );
});

El código anterior no es seguro porque un hacker podría haber comprometido la variable someUnsanitizedData para incluir código JavaScript malicioso, como en el siguiente ejemplo:

someUnsanitizedData = "</scrïpt><scrïpt>alert('You are compromised!')</scrïpt>

Para prevenir ataques XSS, usa un sanitizador de HTML como DomPurify.

Usa tipos opacos

Un tipo de datos opaco aplica el ocultamiento de información. Su estructura de datos no se define en la interfaz, lo que oculta y encapsula la implementación de un tipo de datos concreto. Los módulos externos pueden usar el tipo opaco sin acceder a sus elementos internos, mientras que las funciones internas que tienen acceso a la información oculta pueden manipularlo. Los tipos opacos te permiten cambiar y modificar los detalles internos sin alterar el código que los usa. Por eso, usarlos es una práctica recomendada de desarrollo.

Aunque TypeScript no incluye tipos opacos de forma predeterminada, podemos implementarlos fácilmente. Veamos cómo resolver un caso de uso real.

Imagina una aplicación de comercio electrónico que usa una función en TypeScript para agregar un producto al carrito del cliente:

function addToCart(customerCode: string, productCode: string) {
  console.log(`Product ${productCode} has been added to the cart of the customer ${customerCode}`);
}

Estos parámetros tipados garantizan que el programador no proporcione números ni otros tipos en lugar de cadenas para los parámetros customerCode y productCode:

addToCart('ABC-001984', 'SPC-004487');

Aunque el código no tiene nada de malo, el tipo no expresa los valores con precisión. No puedes saber cuál parámetro es el código del cliente o del producto con solo mirar la línea de código, y TypeScript no mostrará una advertencia si se intercambian ambos valores.

Ahora observa cómo se refactorizó addToCart en el siguiente ejemplo para usar tipos opacos:

export type CustomerCode = string & { _: 'CustomerCode' };
export type ProductCode = string & { _: 'ProductCode' };

const makeCustomerCode =
  (customerCode: string): CustomerCode => {
    if (/^\w{3}-\d{6}$/.test(customerCode)) { //regex validation
      return customerCode as CustomerCode;
    } else {
      throw new Error('Not a customer code!');
    }
  };

const makeProductCode =
  (productCode: string): ProductCode => {
    if (/^\w{3}-\d{6}$/.test(productCode)) { //regex validation
      return productCode as ProductCode;
    } else {
      throw new Error('Not a product code!');
    }
  };

function addToCart(customerCode: CustomerCode, productCode: ProductCode) {
  console.log(`Product ${productCode} has been added to the cart of the customer ${customerCode}`);
}

let customerCode: CustomerCode = makeCustomerCode('ABC-001984');
let productCode: ProductCode = makeProductCode('SPC-004487');

addToCart(customerCode, productCode);

Gracias a los nuevos tipos opacos y a su validación con expresiones regulares, el código genera un error de compilación si proporcionas códigos incorrectos:

let customerCode: CustomerCode = makeCustomerCode('000-001984'); //Error: Not a customer code!
let productCode: ProductCode = makeProductCode('9871'); //Error: Not a product code!

Cuándo usar dangerouslySetInnerHTML y cómo aplicar prácticas adecuadas de sanitización

React usa un DOM virtual como estrategia ligera para actualizar el HTML de la página de manera eficiente y evitar que los usuarios tengan que manipular directamente los elementos HTML mediante las API nativas del navegador.

Sin embargo, a veces quizá tengas que anular este mecanismo y establecer código HTML sin procesar en tus aplicaciones de React. Para manipular este código HTML directamente, React usa una propiedad especial del componente llamada dangerouslySetInnerHTML:

return (
<p dangerouslySetInnerHTML={{__html: data}}></p>);

Como su nombre lo indica, la propiedad dangerouslySetInnerHTML expone a una aplicación a vulnerabilidades si no se usa correctamente. Los hackers pueden aprovechar las aplicaciones que usan dangerouslySetInnerHTML para realizar ataques de cross-site scripting (XSS) e inyectar scripts maliciosos en tu sitio web, disfrazados de datos confiables ingresados por los usuarios.

Para evitar este riesgo, siempre sanitiza el contenido HTML antes de insertarlo, a fin de eliminar las «impurezas» o el código malicioso. Puedes usar una biblioteca como DOMPurify y aplicar la función sanitize:

import DOMPurify from 'dompurify';

return (
<p dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(data)}}></p>);

Es importante tener en cuenta que, como cualquier otra biblioteca, DOMPurify puede presentar vulnerabilidades de seguridad ocasionales. Por suerte, la Snyk Vulnerability Database incorporó rápidamente los datos de esta vulnerabilidad y proporcionó una solución. Por eso, independientemente del historial de estabilidad de una biblioteca, debemos tomar precauciones y usar una herramienta como Snyk para analizar periódicamente las vulnerabilidades conocidas.

Y esa es solo una de las preocupaciones de seguridad que los desarrolladores deben tener en cuenta. En este video, Liran Tal analiza cómo los desarrolladores de React pueden cometer errores que den lugar a otras vulnerabilidades que los hackers podrían explotar.

Liran Tal - You thought your React application is secure? Think again | ReactNext 2021

Conclusión

En este artículo, analizamos algunas prácticas recomendadas para desarrollar aplicaciones de React con TypeScript, describimos posibles riesgos de seguridad y presentamos soluciones.

  • El modo estricto permite aplicar restricciones de tipos y detectar errores de incompatibilidad de tipos al inicio del desarrollo. Usar void en los callbacks evita que los desarrolladores usen el valor de retorno de callbacks que no devuelven ningún valor.

  • Los ataques de inyección de comandos son una amenaza grave para la seguridad. Evita crear código dinámico a partir de entradas de usuario sospechosas y usa la función execFile en lugar de exec.

  • Las inyecciones de HTML son otra amenaza grave para la seguridad. Cuando uses dangerouslySetInnerHTML, siempre sanitiza el marcado que insertes para evitar que los atacantes manipulen las entradas de los usuarios e inyecten código malicioso.

  • Por último, los tipos opacos ofrecen dominios significativos y ayudan a validar tipos con mayor facilidad y a evitar duplicados.

Es fácil usar TypeScript y React para crear aplicaciones rápidas y seguras, especialmente con frameworks como Create React App y aprovechando los conocimientos previos de JavaScript.

Leer más

Blog

Los modelos de frontera encontraron las vulnerabilidades. Solo el atacante encontró las cadenas.

El análisis estático encontró las fallas, pero solo las pruebas de ataque en vivo demostraron cómo podían encadenarse para provocar brechas. Una comparación de Evo COS, Claude Security y Claude Code Security.

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

Blog

Por qué los agentes de programación con IA siguen generando fallas de control de acceso

Los agentes de programación con IA pueden generar lógica de autorización que compila y supera la revisión, pero expone los datos de un inquilino a otro. Descubre por qué es difícil detectar el control de acceso roto y cómo prevenirlo.