¿Guardas datos sensibles en Local Storage? Descubre por qué es una mala práctica.

¿Guardas datos sensibles en Local Storage? Descubre por qué es una mala práctica.


Autor

Actualizado:


¿Mala practica?

Cuando hablamos de mala practica, no hablamos que no se deba usar, puesto que localStorage es una herramienta imprescindible para todos los desarrolladores. A menudo, los desarrolladores pueden verse tentados a utilizar Local Storage por su conveniencia y facilidad de acceso, pero esta práctica conlleva riesgos significativos de seguridad.

Los riegos de seguridad que podemos presentar son los ataques de Cross-Site Scripting (XSS), donde un atacante puede inyectar un script malicioso en una página web para robar información almacenada en Local Storage, esto si no tenemos blindado nuestros datos.

Tranquilo, no todo son malas noticias!, te voy a enseñar varias maneras para que puedas almacenar tus datos de forma segura en el navegador, y como acceder a dichos datos.

localstorage-feature-700x265.png

Este artículo está destinado a educar a desarrolladores, diseñadores de sistemas y entusiastas de la tecnología sobre por qué el almacenamiento de datos sensibles en Local Storage es una mala práctica y cómo pueden mitigar los riesgos asociados adoptando enfoques más seguros.

Alternativas a localStarge

Cookies

Las cookies son pequeñas piezas de datos almacenadas en el navegador del usuario que se utilizan para mantener el estado entre páginas o visitas. En aplicaciones React, las cookies pueden ser útiles para almacenar preferencias del usuario, tokens de sesión, y otros datos que necesitan persistir más allá de la vida útil de una sola página.

Para un mejor uso con React.js es recomendable usar:

npm install js-cookie

Luego, en tu componente React, puedes establecer una cookie de la siguiente manera:

SaveCookie.jsx
import Cookies from 'js-cookie';

function login() {
  // ... lógica de autenticación

  // Una vez autenticado, establece una cookie con el token de sesión
  Cookies.set('session_token', yourToken, { expires: 7 }); // Expira en 7 días
}

Obtener una Cookie:

Para recuperar los datos almacenados en una cookie, se puede hacer lo siguiente:

GetCookie.jsx
function getSessionToken() {
  const token = Cookies.get('session_token');
  return token;
}

Eliminar una Cookie:

Cuando necesites borrar una cookie, por ejemplo, durante un logout, puedes hacerlo así:

DeleteCookie.jsx
function logout() {
  // ... lógica de cierre de sesión

  // Eliminar la cookie de sesión
  Cookies.remove('session_token');
}

Consideraciones de Seguridad:

Cuando se utilizan cookies para almacenar información sensible, es importante asegurarse de que se marquen como Secure y HttpOnly para protegerlas contra accesos no autorizados, como ataques de tipo XSS. La opción Secure asegura que la cookie sólo se enviará con solicitudes HTTPS, y HttpOnly impide que las cookies sean accesibles a través de JavaScript en el navegador, lo cual es especialmente importante para las cookies de sesión.

LocalStorage encriptado

Esta técnica es muy utilizada por las grandes compañías como Facebook, Twitter y Google para no llenar las Cookies de tanta información y poder tener la gran capacidad de almacenar datos de localStorage.

Primero debemos designar un espacio en la estructura de nuestro proyecto bien sea en Helpers, Utils o si lo prefieres con hooks.

Ejemplo de estructuración de proyecto:

|- - Utils/
| | - LocalStorage.jsx
| └ - Key.json

Generar una clave de encriptación segura

Lo más importante para encriptar nuestros datos es tener una clave de encriptación segura, muchos pensaran, pero yo la puedo generar en backend y devolverla al cliente cada que necesite encriptar datos, y sí puede ser una opción pero a su vez también puede ser un problema, puesto que alguien que conozca de la web y entre al network se dará cuenta cual es la clave que retorna back, por eso he creado un algoritmo que nos genera una clave de manera segura y dividina en multiples partes:

GenerateSecretKey.js
const generateSecureKey = (numParts, keyLength) => {
    // Genera una clave segura aleatoria con la longitud especificada
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*+-?';
    let key = '';
    for (let i = 0; i < keyLength; i++) {
        key += characters.charAt(Math.floor(Math.random() * characters.length));
    }

    // Divide la clave en partes iguales
    const partLength = Math.ceil(key.length / numParts);
    let keyParts = [];
    for (let i = 0; i < numParts; i++) {
        keyParts.push(key.substring(i * partLength, (i + 1) * partLength));
    }

    // Mezcla las partes de la clave y guarda el orden de mezcla
    const mixOrder = keyParts.map((part, index) => index);
    for (let i = mixOrder.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [keyParts[i], keyParts[j]] = [keyParts[j], keyParts[i]];
        [mixOrder[i], mixOrder[j]] = [mixOrder[j], mixOrder[i]];
    }

    const instructions = mixOrder.join(',');

    return {
        keyParts: keyParts,
        instructions: instructions
    };
}

Este algoritmo nos genera una llave dividida en diferentes partes y una longitud adaptable a gusto de cada desarrollador.

Para generar la encriptación de los datos debemos hacer uso de una libreria para encriptar datos del lado del front la cual es: crypto-js

Abrimos la consola y nos aseguramos que estemos en el directorio de nuestro proyecto y ejecutamos el siguiente comando:

npm i crypto-js

Con esto instalamos crypto en nuestro proyecto, y procedemos a configurar LocalStorage.jsx en nuestro proyecto:

LocalStorage.jsx
import CryptoJS from "crypto-js";
import Config from "./Key.json"

const reconstructKey = (shuffledData) => {
    const order = shuffledData.instructions.split(',').map(Number);
    let sortedKeyParts = new Array(order.length);
    for (let i = 0; i < order.length; i++) {
        sortedKeyParts[order[i]] = shuffledData.keyParts[i];
    }
    return sortedKeyParts.join('');
}

export const encryptInformation = (data) => {
  let dataCifred = JSON.stringify(data)
  if(Config.isProd) {
    const info = dataCifred
    const metaKey = reconstructKey(Config.secretKey)
    dataCifred = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(info), metaKey).toString();
  }
  return dataCifred
};

export const decryptInformation = (data) => {
  let dataCifred = data
  if(data != null) {
    if(Config.isProd) {
      const metaKey = reconstructKey(Config.secretKey)
      const tmpData = CryptoJS.AES.decrypt(data, metaKey);
      dataCifred = tmpData.toString(CryptoJS.enc.Utf8);
    }
  }
  let finalData = ""
  try {
      finalData = JSON.parse(dataCifred)
  } catch {
      finalData = dataCifred
  }
  return finalData
};

Key.json
{
    "secretKey": {
        "keyParts": [
            "?tS7Hy^T5ftZ",
            "XO+hdwgBlWoVE&HqIp",
            "bm3RTMnqno1!r3^O^-",
            "oNF?FkU?c@eazdOqBB",
            "7OtpMD3xdkURFRDj1x",
            "rEZ8u3n8NbxcLW9U$8",
            "*^KstO6pug#w2OS0fV"
        ],
        "instructions": "6,0,1,3,2,5,4"
    },
    "isProd": true
}

Este fichero LocalStorage.jsx contiene las funciones para armar la contraseña, encriptar y desencriptar datos, con la configuración isProd, podemos controlar cuando estamos en modo desarrollo o en modo producción esto con el fin de que en desarrollo podamos ver los datos sin encriptar para hacer una buena depuración.

Lo mejor de esto, es que podemos fusionar las dos técnicas de guardado de datos para darle una seguridad más grande a nuestros datos sensibles, guardandolos en las Cookies utilizando el modelo de encriptación para localStorage.

Coming soon: Vídeo de implementación tutorial utilizando ambas técnicas.