Cómo implementar cookies seguras solo HTTP en Node.js con Express

Con Express.js, aprenda a implementar cookies que sean seguras en el navegador para evitar ataques XSS (secuencias de comandos entre sitios), ataques de intermediarios y ataques XST (seguimiento entre sitios).

Las cookies son una técnica inteligente para compartir datos entre el navegador de un usuario y su servidor. Los datos contenidos en una cookie pueden ser cualquier cosa que desee:un token de inicio de sesión, algunos datos de perfil o incluso algunos datos de comportamiento que explican cómo el usuario utiliza su aplicación. Desde la perspectiva de un desarrollador, esto es excelente, pero si no está al tanto de los problemas de seguridad comunes, el uso de cookies puede significar la filtración accidental de datos a los atacantes.

La buena noticia:si conoce las técnicas necesarias para proteger las cookies en su aplicación, el trabajo que debe realizar no es demasiado difícil. Hay tres tipos de ataques contra los que debemos protegernos:

  1. Ataques de secuencias de comandos entre sitios (XSS) - Estos ataques se basan en la inyección de JavaScript del lado del cliente en el front-end de su aplicación y luego acceden a las cookies a través de la API de cookies de JavaScript del navegador.
  2. Ataques de intermediario - Estos ataques ocurren cuando una solicitud está en tránsito (viajando desde el navegador al servidor) y el servidor no. tener una conexión HTTPS (sin SSL).
  3. Ataques de rastreo entre sitios (XST) - En el protocolo HTTP, un método HTTP llamado TRACE existe lo que permite a los atacantes enviar una solicitud a un servidor (y obtener sus cookies) sin pasar por ningún tipo de seguridad. Si bien los navegadores modernos generalmente hacen que esto sea irrelevante debido a la desactivación de TRACE método, todavía es bueno estar al tanto y protegerse para mayor seguridad.

Para comenzar, vamos a echar un vistazo a la configuración del servidor donde se crearán nuestras cookies y luego se devolverán al navegador.

Creación de cookies seguras

Para dar contexto a nuestro ejemplo, vamos a usar CheatCode Node.js Boilerplate que nos configura con un servidor Express ya configurado y listo para el desarrollo. Primero, clone una copia del modelo en su computadora:

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git

A continuación, asegúrese de instalar las dependencias repetitivas:

cd nodejs-server-boilerplate && npm install

Después de eso, continúe e inicie el servidor:

npm run dev

A continuación, abramos el /api/index.js archivo en el proyecto. Agregaremos una ruta de prueba donde configuraremos nuestras cookies y verificaremos que estén funcionando:

/api/index.js

import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  // Our cookie code will go here.
};

A continuación, agreguemos el código para configurar nuestra cookie y luego analicemos cómo y por qué. está funcionando:

/api/index.js

import dayjs from "dayjs";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.use("/cookies", (req, res) => {
    const dataToSecure = {
      dataToSecure: "This is the secret data in the cookie.",
    };

    res.cookie("secureCookie", JSON.stringify(dataToSecure), {
      secure: process.env.NODE_ENV !== "development",
      httpOnly: true,
      expires: dayjs().add(30, "days").toDate(),
    });

    res.send("Hello.");
  });
};

Se agregaron muchos detalles, así que repasemos. Primero, en la parte superior del archivo, agregamos una importación para el dayjs Paquete NPM. Esta es una biblioteca para crear y manipular fechas en JavaScript. Usaremos esto a continuación para generar la fecha de caducidad de nuestra cookie para asegurarnos de que no permanezca indefinidamente en un navegador.

A continuación, usamos el Express app instancia (pasada a este archivo a través de /index.js archivo en la raíz del proyecto) para llamar al .use() método que nos permite definir una ruta en nuestra aplicación Express. Para ser claros, esto es puramente por ejemplo. En su propia aplicación, esta podría ser cualquier ruta en la que desee establecer una cookie y devolverla al navegador.

Dentro de la devolución de llamada para nuestro /cookies ruta, nos ponemos a trabajar configurando nuestra cookie. Primero, definimos un ejemplo dataToSecure objeto con algunos datos de prueba dentro.

A continuación, configuramos nuestra cookie. Usando el res.cookie() método proporcionado en Express, pasamos tres argumentos:

  1. El nombre de la cookie que queremos establecer en el navegador (aquí, secureCookie , pero esto podría ser lo que quieras, por ejemplo, pizza ).
  2. La versión en cadena de los datos que queremos enviar. Aquí, tomamos nuestro dataToSecure objeto y encadenarlo usando JSON.stringify() . Recuerde:si los datos que envía al navegador ya son una cadena, no necesito hacer esto.
  3. La configuración de la cookie. Las propiedades establecidas aquí (secure , httpOnly y expires ) son propiedades específicas de Express, pero los nombres se asignan 1:1 con la configuración real en la especificación HTTP.

Centrándonos en el último argumento, la configuración, aquí es donde entra en juego nuestra seguridad. Hay tres configuraciones que son importantes para proteger una cookie:

Primero, el secure La propiedad toma un valor booleano (verdadero/falso) que especifica si esta cookie solo se puede recuperar a través de una conexión SSL o HTTPS. Aquí, configuramos esto según el entorno en el que se ejecuta nuestra aplicación. Siempre que el entorno no desarrollo, queremos forzar que esto sea true . En el desarrollo, esto no es necesario porque nuestra aplicación no está expuesta a Internet, solo nosotros, y es probable que no tenga un servidor proxy SSL configurado localmente para manejar estas solicitudes.

En segundo lugar, el httpOnly La propiedad también toma un valor booleano (verdadero/falso), especificando aquí si las cookies deben ser accesibles o no a través de JavaScript en el navegador. Esta configuración está forzada a true , porque garantiza que cualquier ataque de secuencias de comandos entre sitios (XSS) sea imposible. No tenemos que preocuparnos por el entorno de desarrollo aquí ya que esta configuración no tener una dependencia de SSL o cualquier otra característica del navegador.

En tercer y último lugar, el expires propiedad nos permite establecer una fecha de caducidad en nuestra cookie. Esto nos ayuda con la seguridad al garantizar que nuestra cookie no quedarse en el navegador de un usuario indefinidamente. Dependiendo de los datos que almacene en su cookie (y las necesidades de su aplicación), es posible que desee acortar o ampliar esto. Aquí, usamos el dayjs biblioteca que importamos anteriormente, diciéndole que "obtenga la fecha actual, agregue 30 días y luego devuélvanos un código JavaScript Date objeto para esa fecha". En otras palabras, esta cookie caducará en 30 días desde el punto de creación.

Finalmente, en la parte inferior de la función de devolución de llamada de nuestra ruta, llamamos a res.send() para responder a nuestra solicitud. Porque estamos usando res.cookie() automáticamente le estamos diciendo a Express que envíe la cookie como parte de la respuesta, no es necesario que haga nada más.

Manejo de solicitudes TRACE

Como mencionamos anteriormente, antes de comprobar que nuestras cookies funcionan como se esperaba, queremos asegurarnos de que hemos bloqueado el potencial de TRACE peticiones. Necesitamos hacer esto para asegurarnos de que los atacantes no puedan utilizar el TRACE Método HTTP para acceder a nuestro httpOnly cookies (TRACE no respeta esta regla). Para hacerlo, nos basaremos en un middleware Express personalizado que bloqueará automáticamente TRACE solicitudes de cualquier cliente (navegador u otro).

/middleware/requestMethod.js

export default (req, res, next) => {
  // NOTE: Exclude TRACE and TRACK methods to avoid XST attacks.
  const allowedMethods = [
    "OPTIONS",
    "HEAD",
    "CONNECT",
    "GET",
    "POST",
    "PUT",
    "DELETE",
    "PATCH",
  ];

  if (!allowedMethods.includes(req.method)) {
    res.status(405).send(`${req.method} not allowed.`);
  }

  next();
};

Convenientemente, el código anterior existe como parte de CheatCode Node.js Boilerplate y ya está configurado para ejecutarse dentro de /middleware/index.js . Para explicar lo que está pasando aquí, lo que estamos haciendo es exportar una función que anticipa un Express req objeto, res objeto, y next método como argumentos.

A continuación, definimos una matriz que especifica todos los métodos HTTP permitidos para nuestro servidor. Observe que esta matriz no incluir el TRACE método. Para poner esto en uso, ejecutamos una verificación para ver si este allowedMethods matriz incluye el actual req método de uest. Si no , queremos responder con un código de respuesta HTTP 405 (el código técnico para "método HTTP no permitido").

Suponiendo que el req.method es en el allowedMethods array, llamamos al next() Método pasado por Express que indica a Express que siga avanzando la solicitud a través de otro middleware.

Si desea ver este middleware en uso, comience en el /index.js archivo para ver cómo el middleware() se importa y llama (pasando el Express app instancia) y luego abra el /middleware/index.js archivo para ver cómo el /middleware/requestMethods.js el archivo se importa y se utiliza.

Verificación de cookies seguras en el navegador

Ahora, deberíamos estar listos para probar nuestra cookie. Porque estamos configurando la cookie en la ruta /cookies , necesitamos visitar esta ruta en un navegador para verificar que todo funcione. En un navegador web, abre http://localhost:5001/cookies y luego abra la consola de su navegador (generalmente accesible a través de un CTRL + click en MacOS o haciendo clic derecho en Windows):

En este ejemplo, usamos el navegador Brave, que tiene una herramienta de inspección para desarrolladores idéntica a la de Google Chrome (Firefox y Safari tienen interfaces de usuario comparables, pero es posible que no usen exactamente el mismo nombre que mencionamos a continuación). Aquí, podemos ver nuestro secureCookie siendo configurado, junto con todos los datos y configuraciones que pasamos en el servidor. Para que quede claro, observe que aquí porque estamos en un development entorno, Secure está desarmado.

Una configuración adicional que hemos dejado aquí SameSite también está deshabilitado (esto tiene un valor predeterminado de Lax ) en el navegador. SameSite es otro valor booleano (verdadero/falso) que decide si nuestra cookie solo debe ser accesible en el mismo dominio. Esto está deshabilitado porque puede agregar confusión si está usando un front-end y un back-end separados en su aplicación (si está usando los repetitivos Next.js y Node.js de CheatCode para su aplicación, esto será cierto). Si desea habilitar esto, puede agregar sameSite: true al objeto de opciones que pasamos a res.cookie() como tercer argumento.

Recuperando cookies en el servidor

Ahora que hemos verificado que nuestras cookies existen en el navegador, veamos cómo recuperarlas para usarlas más adelante. Para hacer esto, debemos asegurarnos de que nuestro servidor Express esté analizando galletas. Esto significa convertir la cadena de cookies enviada en los encabezados HTTP de una solicitud a un objeto JavaScript más accesible.

Para automatizar esto, podemos agregar el cookie-parser paquete a nuestra aplicación que nos da acceso a un middleware Express que analiza esto por nosotros:

npm i cookie-parser

Implementar esto es sencillo. Técnicamente, esto ya se usa en CheatCode Node.js Boilerplate que estamos usando para nuestro ejemplo aquí, en el middleware/index.js archivo en la raíz de la aplicación:

/middleware/index.js

[...]
import cookieParser from "cookie-parser";
[...]

export default (app) => {
  [...]
  app.use(cookieParser());
};

Aquí, todo lo que tenemos que hacer es importar cookieParser del cookie-parser paquete y luego llame al app.use() pasando una llamada al cookieParser() método como app.use(cookieParser()) . Para contextualizar esto a nuestro ejemplo anterior, aquí hay una actualización de nuestro /api/index.js archivo (asumiendo que estás escribiendo tu código desde cero):

/api/index.js

import dayjs from "dayjs";
import cookieParser from "cookie-parser";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.use(cookieParser());

  app.use("/cookies", (req, res) => {
    const dataToSecure = {
      dataToSecure: "This is the secret data in the cookie.",
    };

    res.cookie("secureCookie", JSON.stringify(dataToSecure), {
      secure: process.env.NODE_ENV !== "development",
      httpOnly: true,
      expires: dayjs().add(30, "days").toDate(),
    });

    res.send("Hello.");
  });
};

Nuevamente, no necesita hacer esto si está usando CheatCode Node.js Boilerplate.

Con esto implementado, ahora, siempre que la aplicación reciba una solicitud del navegador, sus cookies se analizarán y se colocarán en el req o solicitar objeto en req.cookies como un objeto JavaScript. Dentro de una solicitud, podemos hacer algo como lo siguiente:

/api/index.js

import dayjs from "dayjs";
import cookieParser from "cookie-parser";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.use(cookieParser());

  app.use("/cookies", (req, res) => {
    if (!req.cookies || !req.cookies.secureCookie) {
      const dataToSecure = {
        dataToSecure: "This is the secret data in the cookie.",
      };

      res.cookie("secureCookie", JSON.stringify(dataToSecure), {
        secure: process.env.NODE_ENV !== "development",
        httpOnly: true,
        expires: dayjs().add(30, "days").toDate(),
      });
    }

    res.send("Hello.");
  });
};

Aquí, antes de configurar nuestra cookie de nuestro ejemplo anterior, llamamos a req.cookies (agregado automáticamente para nosotros a través del cookieParser() middleware), verificando si el req.cookies el valor no está definido o, si req.cookies es definido, es req.cookies.secureCookie también definido. Si req.cookies.secureCookie es no definido, queremos seguir adelante y configurar nuestra cookie como normal. Si ya se definió, simplemente respondemos a la solicitud de manera normal, pero omitimos configurar la cookie.

El punto aquí es que podemos acceder a nuestras cookies a través del req.cookies Propiedad en Expreso. No tiene que hacer la verificación anterior en su propia cookie a menos que lo desee.

Cómo gestionar las cookies en GraphQL

Para cerrar el ciclo de administración de cookies, vale la pena comprender cómo hacerlo en relación con un servidor GraphQL. Vale la pena comprender esto si desea establecer o recuperar cookies de un sistema de resolución de GraphQL o durante la instanciación del servidor.

/api/graphql/server.js

import { ApolloServer } from "apollo-server-express";
import schema from "./schema";
import { isDevelopment } from "../../.app/environment";
import { configuration as corsConfiguration } from "../../middleware/cors";

export default (app) => {
  const server = new ApolloServer({
    ...schema,
    introspection: isDevelopment,
    playground: isDevelopment,
    context: async ({ req, res }) => {
      const context = {
        req,
        res,
        user: {},
      };

      return context;
    },
  });

  server.applyMiddleware({
    cors: corsConfiguration,
    app,
    path: "/api/graphql",
  });
};

Aquí, para asegurarnos de que podemos acceder y configurar cookies a través de nuestros solucionadores de consultas y mutaciones de GraphQL, hemos configurado el context propiedad para que el servidor sea igual a una función que toma el req y res (aquí, porque estamos vinculando esto a un Express app ejemplo, estos son los Express req y res objetos) y luego los asigna de nuevo al context objeto que se entrega a todos nuestros solucionadores de consultas y mutaciones:

import dayjs from 'dayjs';

export default {
  exampleResolver: (parent, args, context) => {
    // Accessing an existing cookie from context.req.
    const cookie = context?.req?.cookies?.secureCookie;

    // Setting a new cookie with context.res.
    if (context.res && !cookie) {
      const dataToSecure = {
        dataToSecure: "This is the secret data in the cookie.",
      };

      res.cookie("secureCookie", JSON.stringify(dataToSecure), {
        secure: process.env.NODE_ENV !== "development",
        httpOnly: true,
        expires: dayjs().add(30, "days").toDate(),
      });
    }

    // Arbitrary return value here. This would be whatever value you want to
    // resolve the query or mutation with.
    return cookie;
  },
};

En el ejemplo anterior, repetimos los mismos patrones que antes en el tutorial, sin embargo, ahora estamos accediendo a las cookies a través de context.req.cookies y configurándolos a través de context.res.cookie() . Cabe destacar que este exampleResolver no tiene la intención de ser funcional, es solo un ejemplo de cómo acceder y configurar cookies desde dentro de un resolutor. Su propio sistema de resolución de GraphQL utilizará un código más específico relacionado con la lectura o escritura de datos en su aplicación.

Asegurarse de que las cookies estén incluidas en sus solicitudes de GraphQL

Dependiendo de su elección de cliente GraphQL, es posible que las cookies de su navegador (solo http o de otro tipo) no se incluyan en la solicitud automáticamente. Para asegurarse de que esto suceda, querrá verificar la documentación de su cliente y ver si tiene una opción/configuración para incluir credenciales. Por ejemplo, aquí está la configuración del cliente Apollo de Next.js Boilerplate de CheatCode:

new ApolloClient({
  credentials: "include",
  link: ApolloLink.from([
    new HttpLink({
      uri: settings.graphql.uri,
      credentials: "include",
    }),
  ]),
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      errorPolicy: "all",
      fetchPolicy: "network-only",
    },
    query: {
      errorPolicy: "all",
      fetchPolicy: "network-only",
    },
    mutate: {
      errorPolicy: "all",
    },
  },
});

Aquí, nos aseguramos de establecer el credentials propiedad como 'include' para indicarle a Apollo que queremos que incluya nuestras cookies con cada solicitud. Además, debido a que estamos usando el método HTTP Link de Apollo, en buena medida establecemos credentials a 'include' aquí también.

Terminando

En este tutorial, vimos cómo administrar cookies seguras en Node.js con Express. Aprendimos a definir una cookie usando el secure , httpOnly y expires valores para garantizar que se mantengan separados de los atacantes y cómo deshabilitar TRACE solicitudes para evitar el acceso de puerta trasera a nuestro httpOnly galletas.

También aprendimos cómo acceder a las cookies utilizando Express cookie-parser middleware, aprendiendo a acceder a las cookies en una ruta Express, así como a través de un contexto GraphQL.