Proteja el código del lado de cliente y certifique la autenticidad de la recopilación de datos

David Sénécal

escrito por

David Sénécal

July 08, 2025

David Sénécal

escrito por

David Sénécal

David Sénécal trabaja para Akamai como director de Ingeniería: fraude y abuso, y es autor de The Reign of Botnets. Es un apasionado de la mejora de la seguridad en Internet y es un experto en fraude en línea y detección de bots. Cuenta con más de 25 años de experiencia trabajando con tecnologías de rendimiento web, seguridad y redes empresariales en diversas funciones, entre las que se incluyen asistencia, integración, consultoría, desarrollo, gestión de productos, arquitectura e investigación.

La protección del lado de cliente y su carga útil requiere una estrategia compleja que implica varias capas de defensa y tecnologías.
La protección del lado de cliente y su carga útil requiere una estrategia compleja que implica varias capas de defensa y tecnologías.

Contenido

Es bien sabido que la protección eficaz de las aplicaciones web y los sitios web requiere el uso de JavaScript para recopilar los datos del lado del cliente desde el navegador. Normalmente, se trata de las características del dispositivo y del navegador, las preferencias del usuario (huellas digitales) y los datos que reflejan la interacción del usuario con sus dispositivos, como los movimientos del ratón, la pulsación táctil y las pulsaciones de teclas (telemetría).

Los proveedores y los profesionales de la seguridad web tratan los datos a través de varios métodos de detección (desde reglas simples hasta modelos de IA avanzados) para verificar la probabilidad de que una solicitud legítima se origina a partir de un dispositivo legítimo controlado por una persona.

La combinación de los diferentes puntos de datos también ayuda a diferenciar a los usuarios y a evaluar su actividad a lo largo del tiempo. Este es el principio fundamental que utilizan los productos de gestión de bots y detección de fraude para ayudar a detectar ataques como el Credential Stuffing, el robo de cuentas, el abuso de apertura de cuentas y el scraping de contenido, entre otros.

Integridad de la recopilación de datos

Garantizar la autenticidad e integridad de los datos es clave para evaluar con precisión la interacción del usuario con el sitio e identificar amenazas. ¿Cómo puede alguien afirmar la autenticidad e integridad de los datos sabiendo que cualquier cosa ejecutada en el lado del cliente podría ser manipulada o alterada?

Al ejecutar código JavaScript del lado del cliente, hay dos razones para asegurarse de que el código está bien protegido. 

1. El código JavaScript forma parte de la propiedad intelectual de una organización. Debe protegerse en la medida de lo posible contra los atacantes y los competidores. 

2. La integridad de los datos es fundamental para comprender correctamente el entorno y sus factores de riesgo. El código JavaScript protegido garantiza que los datos sean fiables, porque se recopilaron de forma auténtica mediante la ejecución del script y no se manipularon ni transformaron.

Cómo proteger el código del lado de cliente y garantizar la autenticidad de los datos

Al igual que con cualquier otro aspecto de la seguridad, no existe una solución única para responder al problema. En esta entrada del blog, presentamos una recopilación de métodos que Akamai utiliza para proteger el código JavaScript, aplicar su ejecución y garantizar la autenticidad de los datos recopilados, entre los que se incluyen:

  • Ocultación de código
  • Comprobación de integridad de datos
  • Ocultación de VM
  • Inserción de código adicional y engañoso
  • Rotación de código JavaScript
  • Rotación de campo dinámico
  • Canalización de construcción de JavaScript y validación de datos

Si decide seguir prácticas similares para proteger su propio código, le recomendamos que utilice una combinación de estos métodos en función de las necesidades de su equipo, organización y pila tecnológica.

Ocultación de código

La ocultación es uno de los métodos más comunes utilizados para proteger el código JavaScript. La ocultación dificulta el seguimiento y la comprensión del código.

Las prácticas de desarrollo sólidas recomiendan que la nomenclatura de funciones y variables sea lo más descriptiva posible y que el código esté estructurado de forma lógica para facilitar la depuración y el mantenimiento. Aunque se trata de una práctica valiosa que ahorra tiempo y esfuerzo, el código limpio es un objetivo fácil para la ingeniería inversa.

Cuando se aplica la ocultación, estas buenas prácticas de desarrollo se rompen, y las variables y funciones con nombres descriptivos se sustituyen por otros aleatorios. Pueden reordenarse y codificarse, y es posible que se divida parte de la lógica. Un navegador web puede ejecutar el código sin problemas y el resultado será el mismo. Sin embargo, cualquiera que intente aplicar ingeniería inversa al código lo tendrá más difícil.

Los desarrolladores siguen utilizando código bien estructurado para fines de mantenimiento y mejora. Cuando la nueva versión está lista, el código se ejecuta a través de un motor de ocultación antes de su lanzamiento. Hay varios productos comerciales y de código libre o abierto, como Code Beautify, JScrambler y Digital.ai, disponibles para ocultar de forma rápida y sencilla el código JavaScript.

La Figura 1 es un ejemplo de una función sencilla de JavaScript que se utiliza habitualmente para tomar huellas digitales y se ha diseñado para extraer varias características del dispositivo, que se muestra antes de la ocultación.

  function getDeviceInfo() {
 return {
   userAgent: navigator.userAgent,
   hardwareConcurrency: navigator.hardwareConcurrency || "unknown",
   screenOrientation: screen.orientation.type,
 };
}

Fig. 1: Código original antes de la ocultación

Puede ver lo sencillo que es entender el código en su estado original. Incluso alguien con conocimientos limitados de codificación puede comprender el propósito previsto y entender cómo logra su objetivo.

La Figura 2 es la misma función de JavaScript después de ejecutarse a través de la herramienta en línea Code Beautify.

  (function(_0xbf521e,_0x43c80b){var _0x4ad763=_0x3e09,_0x18fc85=_0xbf521e();while(!![]){try{var_0x40d2a7=parseInt(_0x4ad763(0xfc))/(0x18d1+-0xe6d+-0xa63)+-parseInt(_0x4ad763(0xf6))/(0x2*-0x7e4+0x171a+-0x750)+-parseInt(_0x4ad763(0xfb))/(-0x2e7*-0xb+0x6b*0x1f+-0x2cdf)*(parseInt(_0x4ad763(0xef))/(0x40f*-0x4+-0x897+0x18d7))+-parseInt(_0x4ad763(0xf3))/(0x3*-0xb5f+0x462+0x1dc*0x10)*(parseInt(_0x4ad763(0xf0))/(-0xb87*-0x1+0x18e8+-0x3*0xc23))+-parseInt(_0x4ad763(0xfa))/(0x2258+0x8f7+-0x2b48)*(-parseInt(_0x4ad763(0xee))/(0x3e9+-0xe93+0xab2))+parseInt(_0x4ad763(0xf1))/(0x1*-0x81e+0x525*-0x5+0x4*0x878)+parseInt(_0x4ad763(0xed))/(-0x59*-0x1f+0x779+-0x6f*0x2a);if(_0x40d2a7===_0x43c80b)break;else _0x18fc85['push'](_0x18fc85['shift']());}catch(_0x4460fc){_0x18fc85['push'](_0x18fc85['shift']());}}}(_0x1950,-0x1f*-0x38cb+0x17f2fa+-0x10aebf));function getDeviceInfo(){var _0x7a196=_0x3e09,_0x52340e={'VEDsL':_0x7a196(0xf8)};return{'userAgent':navigator[_0x7a196(0xf4)],'hardwareConcurrency':navigator[_0x7a196(0xf2)+_0x7a196(0xfd)]||_0x52340e[_0x7a196(0xf5)],'screenOrientation':screen[_0x7a196(0xf9)+'n'][_0x7a196(0xf7)]};}function _0x3e09(_0x56cbb3,_0x1167d0){var _0xddc250=_0x1950();return _0x3e09=function(_0x363b57,_0x27d74c){_0x363b57=_0x363b57-(-0x6d9+0x1316*0x1+-0xb50);var _0x1b2eec=_0xddc250[_0x363b57];return _0x1b2eec;},_0x3e09(_0x56cbb3,_0x1167d0);}function _0x1950(){var _0x1d7105=['ncurrency','20162890GviEyp','2488DLGTpn','4rCTHCm','65154TKsGUe','7673175smCphy','hardwareCo','670lOXWEG','userAgent','VEDsL','1749116JlgXKK','type','unknown','orientatio','12971xihUJr','2027775PnQRTc','487370FufNiT'];_0x1950=function(){return _0x1d7105;};return _0x1950();}

Fig. 2: Código ocultado (a través de Code Beautify)

Si no fuera por otra razón que su longitud, el código ocultado es claramente más difícil de entender. El código puede parecer complejo, pero existen métodos para invertir estas técnicas de ocultación más sencillas y los atacantes los entienden bien. Sin embargo, al menos esto eleva el listón para disuadir a los atacantes menos sofisticados y con menos conocimientos.

La mitad de la batalla en materia de seguridad consiste en agotar al atacante o hacer que la perspectiva de atacar a su organización resulte poco atractiva, basándose en el esfuerzo percibido o real que se requiere para orquestar un ataque con éxito.

Comprobación de integridad de datos

Como hemos visto, la ocultación de código es un buen punto de partida, pero no es suficiente por sí sola para disuadir a los atacantes motivados, ya que existen métodos y herramientas de desocultación para revertir el código a su formato original. Además de los métodos de ocultación, la implementación de funciones adicionales de comprobación de integridad de datos y código puede proteger aún más la integridad de la información recopilada.

Las comprobaciones de integridad del código y los datos son pequeñas funciones que se añaden en varias ubicaciones a lo largo del código para comprobar que la salida producida por la secuencia de comandos resulta legítima. Las comprobaciones suelen utilizar diferentes variables, incluida la salida de las funciones principales de JavaScript existentes junto con una semilla única específica de una sesión de usuario para producir una salida secundaria.

La Figura 3 es un ejemplo de una función que toma tres variables como entrada, utiliza las variables dentro de una fórmula matemática simple y una función hash, y devuelve el resultado. Las variables a y b podrían corresponder a la salida de dos funciones principales, y la variable c podría ser una semilla única. En este ejemplo, todas las propiedades deben ser valores numéricos.

  function IntegrityCheck(a, b, c) {
   const mathResult = a + b * c;
   const stringResult = String(mathResult);
   let hash = 0;
   for (let i = 0; i < stringResult.length; i++) {
    hash = (hash * 31 + stringResult.charCodeAt(i)) >>> 0; 
 }
   return hash;
}

Fig. 3: Ejemplo de código con varias variables para la integridad de los datos

Más concretamente, las propiedades screen.colorDepth y navigator.hardwareConcurrency que devuelven valores numéricos se pueden utilizar como variables a y b en la función simple de la Figura 3. Esa función no se limita realmente a las propiedades que devuelven un valor numérico, ya que cualquier valor puede ser hash y transformado en un entero antes de ser introducido en la función de comprobación de integridad. Simplemente se hizo de esa manera para no complicar nuestro ejemplo.

Para mayor diversidad, algunas funciones de comprobación de integridad pueden realizar un hash de la salida de la función principal, como se muestra en el ejemplo de la Figura 4.

  import { createHash } from 'crypto';

function hashTwoVariables(a, b) {
 const concatenatedString = String(a) + String(b); 
 const hash = createHash('sha256').update(concatenatedString).digest('hex');
 return hash;
}

Fig. 4: Ejemplo de salida hash

Puede haber docenas de funciones pequeñas de este tipo, cada una de las cuales realiza operaciones diferentes y consume resultados diferentes de funciones principales repartidas por todo el código para proteger puntos de datos clave. Como comprobación final, también puede firmar toda la carga útil, incluidos todos los datos de huellas dactilares y de comportamiento, así como los resultados de las funciones individuales de comprobación de integridad. Una forma de hacerlo es mediante el hash de toda la carga útil y la comparación de la salida inicial. Si los hashes coinciden tanto en el lado del emisor como en el del receptor, la carga útil se considera segura e inalterada.

Ocultación de VM

Estas funciones sencillas de comprobación de integridad no se pueden dejar abiertas ni ocultarse mediante métodos de ocultación sencillos. Aquí es donde entra en juego la técnica más avanzada de ocultación de máquinas virtuales (VM), que dificulta al atacante comprender los detalles y cómo producir una carga útil válida.

La ocultación de VM transforma el código en código de bytes de máquina virtual: algo que una máquina puede interpretar, pero mucho que resulta más difícil de revertir para los atacantes.

Varios proveedores ofrecen métodos de ocultación de VM, pero la ocultación de VM no siempre admite todos los tipos de lógica de funciones. Cuando utilice la ocultación de VM, siga las directrices del proveedor y realice pruebas de regresión exhaustivas del código.

Las pruebas de regresión son una práctica excelente en general, no solo para la ocultación de máquinas virtuales, y merece la pena implementarlas como parte de la rutina de seguridad. Sin embargo, resulta especialmente útil en combinación con la ocultación de VM, teniendo en cuenta la salida de código complejo del método.

Inserción de código adicional y engañoso

Para ponerle las cosas más difíciles a los atacantes que intentan aplicar ingeniería inversa al código, se añade una capa adicional que consiste en incorporar código que no tiene ninguna función real en la lógica central. El objetivo es despistar a los atacantes, frustrarles y obligarles a tirar la toalla.

Del mismo modo, se puede considerar la posibilidad de variar la estructura de las funciones de comprobación de integridad para dificultar aún más la desocultación y la ingeniería inversa. Una forma de lograrlo es desarrollar varias funciones estructuralmente distintas pero equivalentes que produzcan la misma salida.

Una función funcionalmente idéntica pero estructuralmente diferente dará lugar a una codificación diferente de la función después de haber sido sometida a ocultación de las VM, lo que hará que el código sea mucho más complejo de revertir.

La Figura 5 es un ejemplo de tres funciones de este tipo que siempre devuelven la misma salida, pero todas son ligeramente diferentes.

  function IntegrityCheck_1(a, b) {
 return a + b * 1; 
}

function IntegrityCheck_2(a, b) {
 return a + 0 + b; 
}

function IntegrityCheck_3(a, b, c) {
 return a + b + c * 0; 
}

Fig. 5: Tres ejemplos de código diferente que logran la misma salida

Rotación de código JavaScript

Contar con código engañoso, ocultación avanzada y comprobaciones de integridad es bueno, pero los atacantes pueden ser muy persistentes, y ningún código estancado está libre se resistirá a la ingeniería inversa, dado el tiempo, el esfuerzo y la habilidad. Es decir, a menos que limitemos la validez de script.

Imagine generar miles de iteraciones únicas del mismo código funcionalmente equivalente, cada una con diferentes funciones de comprobación de integridad en cada nueva versión de código JavaScript. Cada iteración solo se utiliza y es válida durante 10 a 20 minutos, y existen controles para obligar al cliente a recargar una nueva iteración con regularidad, lo que provoca que las iteraciones más antiguas queden rápidamente obsoletas e inválidas.

El objetivo de este método es abrumar al atacante con complejidad y superar su eficiencia, de modo que no tengan otra opción que ejecutar el JavaScript a través de un navegador y no se den cuenta de lo que hace el código.

Rotación de campo dinámico

El código puede ser difícil de leer y descifrar, pero a menudo se puede inferir su propósito examinando la salida y los datos recopilados y enviados. Parte de la información enviada al servidor puede ser obvia, especialmente en lo que respecta a detalles como las características del dispositivo y del navegador.

Sin embargo, sería más difícil deducir la intención de las funciones que simplemente devuelven un valor booleano, o de una función de comprobación de integridad que devuelve un entero.

Una forma de hacer que la estructura de la carga útil sea menos predecible y más confusa para los atacantes es cambiar los nombres de los campos utilizados para informar de cada punto de datos recopilado, así como su posición relativa en la carga útil para cada iteración.

Como hemos comentado, cada iteración de JavaScript tiene un conjunto único de comprobaciones de integridad de código. Además, la carga útil utilizará nombres de campo diferentes y la posición de un punto de datos determinado cambia con cada iteración.

Los nombres de los campos y sus posiciones se definen en el momento de la creación de JavaScript sobre la base de un algoritmo predefinido que el servidor que procesa los datos también puede ejecutar para recuperar los diversos elementos de información que son críticos para la detección precisa de bots y fraudes en la ubicación correcta.

La Figura 6 ilustra cómo cada campo y su posición pueden variar de una iteración a otra. Los nombres de los campos no deben ser descriptivos para que sean menos obvios.

  Payload Iteration #1

  mx01: [user-agent]
  mx02: [display-mode]
  mx03: [hardconcur]
  mx04: [pixelDepth]
  mx05: [language]
  mx06: [WebGL_Rend]
  mx07: [intg_chck_1]
 
  Payload Iteration #2

  yw01: [display-mode]
  yw02: [intg_chck_1]
  yw03: [user-agent]
  yw04: [pixelDepth]
  yw05: [hardconcur]
  yw06: [WebGL_Rend]
  yw07: [language]
  
  Payload Iteration #3

  za01: [language]
  za02: [WebGL_Rend]
  za03: [hardconcur]
  za04: [pixelDepth]
  za05: [intg_chck_1]
  za06: [user-agent]
  za07: [display-mode]

Fig. 6: Ejemplos de iteraciones de nombre de campo

Con solo siete campos en la salida (como en el ejemplo anterior), es fácil detectar el cambio de una iteración a otra, pero imagine hacer esto cuando se recopilan y devuelven cientos de puntos de datos.

Canalización de construcción de JavaScript y validación de datos

Los diversos métodos utilizados para proteger el código JavaScript y garantizar la integridad de los datos recopilados requieren el desarrollo de un complejo proceso de creación y publicación. En primer lugar, los desarrolladores actualizarán el archivo JavaScript sin procesar y con formato correcto, probarán la funcionalidad y ejecutarán pruebas de regresión.

A continuación, los desarrolladores utilizarán un algoritmo para generar miles de iteraciones, que producirán versiones únicas con diferentes:

  • Funciones de comprobación de integridad de datos que varían los puntos de datos del JavaScript principal, de las funciones matemáticas/hash utilizadas y de su posición relativa en la lógica general 
  • Conjuntos de código engañoso o no utilizado
  • Nombres de campo de salida de carga útil
  • Órdenes de campo de salida de carga útil

Una vez generados estos componentes únicos, la iteración del archivo JavaScript pasa por los siguientes procesos:

  • Ocultar la comprobación de la integridad de los datos y otras funciones esenciales a través de la VM
  • Ocultar el código general
  • Subir la iteración al servidor web

Una vez generadas y cargadas todas las iteraciones, el nuevo conjunto de JavaScript debe habilitarse en producción. Este cambio se coordina con el servidor que ejecuta el motor de detección de bots y fraudes que recibe los datos. Debe ejecutar parte del algoritmo utilizado en el sistema de compilación de JavaScript para poder:

  • Validar que el cliente está enviando la carga útil de la iteración de JavaScript actual y no una obsoleta
  • Analizar los diferentes campos de la carga útil según la iteración de JavaScript con la que se generó
  • Validar los valores de comprobación de integridad del código ejecutando funciones equivalentes

El producto final, con la ocultación final, se debe probar completamente de principio a fin en la fase de preproducción antes de su lanzamiento para garantizar que todos los componentes están sincronizados y producen el resultado esperado. Esto requiere la creación de un flujo de trabajo de compilación algo complejo para JavaScript.

Sin embargo, cuando su contenido debe preservarse de la competencia curiosa y de los atacantes, y su resultado afecta a la seguridad de los usuarios en Internet y los sitios web que visitan, se convierte en un esfuerzo que merece la pena.

Conclusión

Se debe proteger el código JavaScript que se ejecuta en el lado del cliente, que se utiliza para recopilar huellas dactilares y telemetría, y la lógica personalizada diseñada para detectar bots y fraudes. Existen varias estrategias para proteger el código y los datos, pero la implementación de una o dos solo proporcionará una protección marginal contra los atacantes más sofisticados.

Para proteger el código del lado del cliente y su carga útil se necesita una estrategia compleja que implique varias capas de defensa y tecnologías, incluidas ocultación de código, código engañoso o no utilizado, funciones de comprobación de la integridad del código combinadas con la ocultación de máquinas virtuales, asignación aleatoria de la estructura de carga útil para que sea menos predecible y actualización periódica del código.

La ecuación de la Figura 7 resume la complejidad de la combinación general de estrategias que se deben desarrollar para garantizar una protección eficaz.

  [JS Code obfuscation[
  + Misleading code 
  + unused code
  + VM Obfuscation [code integrity check] 
  + unique field names
  + field relative position shift] 
  x  [Number of unique iterations] 
  + Limited version validity (10 minutes)
  + Force JS reload]

Fig. 7: Ecuación de estrategias de protección de JavaScript

En última instancia, esta combinación obliga al cliente a ejecutar JavaScript, lo que reduce su oportunidad de manipular los datos y derrotar el motor de detección. Para limitar el esfuerzo de desarrollo, se recomiendan soluciones comerciales para algunos de los pasos más complejos, como la ocultación de máquinas virtuales. Sin embargo, algunas estrategias, como el uso de comprobaciones de integridad del código, fragmentos de código engañosos y varias iteraciones, se deben crear y mantener internamente para proporcionar protección en caso de que los atacantes desarrollen un desocultador.



David Sénécal

escrito por

David Sénécal

July 08, 2025

David Sénécal

escrito por

David Sénécal

David Sénécal trabaja para Akamai como director de Ingeniería: fraude y abuso, y es autor de The Reign of Botnets. Es un apasionado de la mejora de la seguridad en Internet y es un experto en fraude en línea y detección de bots. Cuenta con más de 25 años de experiencia trabajando con tecnologías de rendimiento web, seguridad y redes empresariales en diversas funciones, entre las que se incluyen asistencia, integración, consultoría, desarrollo, gestión de productos, arquitectura e investigación.