Protégez le code côté client et certifiez l'authenticité de la collecte de données
Contenu
Il est bien connu que la protection efficace des applications Web et des sites Web nécessite l'utilisation de JavaScript pour la collecte de données côté client à partir du navigateur. Il s'agit généralement des caractéristiques du terminal et du navigateur, des préférences utilisateur (empreintes digitales) et des données qui reflètent l'interaction de l'utilisateur avec son terminal, comme les mouvements de la souris, les contacts tactiles et les frappes clavier (télémétrie).
Les spécialistes de la sécurité Web et les fournisseurs traitent les données via diverses méthodes de détection (des règles simples aux modèles d'IA avancés) pour vérifier la probabilité qu'une demande légitime provienne d'un terminal légitime contrôlé par un humain.
La combinaison des différents points de données permet également de différencier les utilisateurs et d'évaluer leur activité au fil du temps. C'est le principe fondamental que les outils de gestion des bots et de détection des fraudes utilisent pour identifier des attaques telles que le« credential stuffing », la prise de contrôle de compte, l'ouverture de compte abusive et le scraping de contenu, entre autres.
Intégrité de la collecte de données
Il est essentiel de garantir l'authenticité et l'intégrité des données pour évaluer avec précision l'interaction de l'utilisateur avec le site et signaler les menaces. Comment quelqu'un peut-il certifier l'authenticité et l'intégrité des données tout en sachant que tout ce qui est exécuté côté client pourrait être altéré et manipulé ?
Lors de l'exécution du code JavaScript côté client, il existe deux raisons majeures pour lesquelles ce code doit être bien protégé.
1. Le code JavaScript fait partie de la propriété intellectuelle d'une organisation. Il doit être protégé autant que possible contre les acteurs malveillants et les concurrents.
2 l'intégrité des données est essentielle pour bien comprendre l'environnement et ses facteurs de risque. Le JavaScript protégé garantit la fiabilité des données, car elles ont été réellement collectées en exécutant le script et n'ont pas été manipulées ni transformées.
Comment protéger le code côté client et garantir l'authenticité des données
Comme pour tout autre aspect de la sécurité, il n'existe pas de solution unique pour résoudre le problème. Dans cet article de blog, nous présentons un ensemble de méthodes utilisées par Akamai pour protéger le code JavaScript, appliquer son exécution et garantir l'authenticité des données collectées, notamment :
- Obscurcissement de code
- Contrôle de l'intégrité des données
- Obscurcissement par la machine virtuelle (VM)
- Insertion de code trompeuse et supplémentaire
- Rotation du code JavaScript
- Rotation de champ dynamique
- Pipeline de création JavaScript et validation des données
Si vous décidez d'adopter des pratiques similaires pour protéger votre propre code, nous vous recommandons d'utiliser une combinaison de ces méthodes en fonction des besoins de votre équipe, de votre organisation et de votre stack technologique.
Obscurcissement de code
L'obscurcissement est l'une des méthodes les plus couramment utilisées pour protéger le code JavaScript. Il rend le code plus difficile à suivre et à comprendre.
Les bonnes pratiques de développement recommandent de nommer les fonctions et les variables de manière aussi descriptive que possible, et de structurer le code de façon logique afin de faciliter le débogage et la maintenance. Bien que cette pratique permette de gagner du temps et des efforts, un code propre est une cible facile pour l'ingénierie inverse.
Lorsque l'obscurcissement est appliqué, ces bonnes pratiques de développement sont rompues, et les noms descriptifs des fonctions et des variables sont remplacés par des noms aléatoires. Ils peuvent être réordonnés et codés, et une partie de la logique peut être divisée. Un navigateur Web peut toujours exécuter le code sans problème, et le résultat sera le même. Cependant, toute personne tentant de rétroconcevoir le code aura plus de difficultés.
Les développeurs utilisent toujours un code bien structuré pour la maintenance et les améliorations. Une fois qu'une nouvelle version est prête, le code passe par un moteur d'obscurcissement avant d'être publié. Plusieurs produits gratuits/open source et commerciaux, tels que Code Beautify, JScrambler et Digital.ai, sont disponibles pour brouiller rapidement et facilement le code JavaScript.
La figure 1 est un exemple de fonction JavaScript simple couramment utilisée lors de la prise d'empreintes digitales, conçue pour extraire diverses caractéristiques du terminal, présentée avant obscurcissement.
function getDeviceInfo() {
return {
userAgent: navigator.userAgent,
hardwareConcurrency: navigator.hardwareConcurrency || "unknown",
screenOrientation: screen.orientation.type,
};
}
Fig. 1 : Code d'origine avant obscurcissement
Vous pouvez voir à quel point il est simple de comprendre le code dans son état d'origine. Même une personne ayant des connaissances limitées en matière de codage peut comprendre l'objectif visé et la manière dont il atteint son objectif.
La figure 2 représente la même fonction JavaScript après avoir été traitée par l'outil en ligne 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 : Code brouillé (via Code Beautify)
Ne serait-ce qu'en raison de sa longueur, le code brouillé est nettement plus difficile à comprendre. Le code peut sembler complexe, mais les méthodes permettant d'inverser ces techniques d'obscurcissement plus simples existent et sont bien comprises par les acteurs de menace. Mais au moins, cela élève le niveau de sécurité pour dissuader les acteurs les moins sophistiqués et les moins bien informés.
Une grosse partie du travail en matière de sécurité consiste à épuiser l'acteur malveillant et/ou à rendre peu attrayante la perspective de cibler votre organisation, en fonction de l'effort, perçu ou réel, nécessaire pour mener une attaque réussie.
Contrôle de l'intégrité des données
Comme nous l'avons vu, l'obscurcissement du code constitue un bon point de départ, mais il ne suffit pas à lui seul pour dissuader les acteurs malveillants motivés, car il existe des méthodes et des outils de clarification permettant de rétablir le code dans son format d'origine. En plus des méthodes d'obscurcissement, la mise en œuvre de fonctions supplémentaires de code et de contrôle d'intégrité des données peut encore protéger l'intégrité des informations collectées.
Les contrôles d'intégrité du code et des données sont de petites fonctions ajoutées à différents emplacements du code pour vérifier que la sortie produite par le script est en effet légitime. Les vérifications utilisent généralement plusieurs variables, y compris la sortie des fonctions JavaScript de base existantes, ainsi qu'un Seed unique spécifique à une session utilisateur pour produire une sortie secondaire.
La figure 3 est un exemple de fonction qui prend trois variables en entrée, utilise ces variables dans une formule mathématique simple ainsi qu'une fonction de hachage, puis renvoie le résultat. Les variables a et b pourraient correspondre à la sortie de deux fonctions principales, et la variable c pourrait être une graine unique. Dans cet exemple, toutes les propriétés doivent être des valeurs numériques.
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 : Exemple de code avec plusieurs variables pour l'intégrité des données
Plus concrètement, les propriétés screen.colorDepth et navigator.hardwareConcurrency, qui renvoient toutes deux des valeurs numériques, pourraient être utilisées comme variables a et b dans la fonction simple de la Figure 3. Cette fonction n'est pas limitée aux propriétés qui renvoient une valeur numérique, car toute valeur peut être hachée et transformée en un entier avant d'être transmise à la fonction de contrôle d'intégrité. C'était ce que nous avons fait pour illustrer notre exemple simple.
Par souci de diversité, certaines fonctions de contrôle d'intégrité peuvent hacher la sortie de la fonction principale, comme illustré dans l'exemple de la figure 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 : Exemple de sortie hachée
Il peut y avoir des dizaines de petites fonctions de ce type, chacune effectuant des opérations différentes et consommant des sorties différentes issues des fonctions principales, dispersées dans le code pour protéger les points de données clés. En guise de vérification finale, on peut aussi « signer » l'ensemble de la charge utile, y compris toutes les données de type empreintes digitales et comportementales, ainsi que les résultats de fonctions de contrôle d'intégrité individuelles. Une manière de procéder consiste à appliquer une fonction de hachage à l'ensemble de la charge utile, puis à comparer la sortie initiale. Si les valeurs de hachage correspondent du côté de l'expéditeur et du récepteur, la charge utile est considérée comme sûre et non altérée.
Obscurcissement par la machine virtuelle (VM)
Ces fonctions de contrôle d'intégrité simples ne peuvent pas être laissées ouvertes ou masquées à l'aide de méthodes d'obscurcissement simples. C'est à ce moment-là qu'intervient la méthode plus avancée d'obscurcissement via machine virtuelle (VM), ce qui complique la tâche de l'acteur malveillant pour comprendre ce qui se passe en arrière-plan et comment générer une charge utile valide.
L'obscurcissement via machine virtuelle transforme le code en bytecode de machine virtuelle, un format qu'une machine peut interpréter, mais beaucoup plus difficile à rétroconcevoir pour les acteurs malveillants.
Plusieurs fournisseurs proposent des méthodes d'obscurcissement via machine virtuelle (VM), mais ces méthodes ne prennent pas toujours en charge tous les types de logique de fonction. Lors de l'utilisation de l'obscurcissement par VM, suivez les directives de votre fournisseur et testez minutieusement votre code en vue de la régression.
Les tests de régression sont une bonne pratique en général, pas seulement pour l'obscurcissement via machine virtuelle, et méritent d'être intégrés à votre routine de sécurité. Cependant, il est particulièrement utile en conjonction avec l'obscurcissement de la VM, compte tenu de la sortie de code complexe de la méthode.
Insertion de code trompeuse et supplémentaire
Pour compliquer davantage la tâche de l'acteur malveillant qui tente de rétroconcevoir le code, une couche supplémentaire peut consister à ajouter du code qui n'a aucune utilité réelle pour la logique centrale. Cette approche vise à détourner les acteurs de la menace, à les frustrer et à les inciter à abandonner leurs efforts.
De même, vous pouvez envisager de modifier la structure des fonctions de vérification d'intégrité pour rendre la clarification et la rétro-ingénierie plus difficiles. Pour ce faire, il est possible de développer plusieurs fonctions structurellement distinctes mais équivalentes qui produisent la même sortie.
Une fonction identique sur le plan fonctionnel mais différente sur le plan structurel produira un encodage différent de la fonction après qu'elle aura subi un obscurcissement via machine virtuelle, rendant le code bien plus complexe à rétroconcevoir.
La figure 5 est un exemple de trois fonctions de ce type qui renvoient toujours la même sortie mais qui sont toutes légèrement différentes.
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 : Trois exemples de codes différents qui obtiennent la même sortie
Rotation du code JavaScript
Avoir du code trompeur, un obscurcissement avancé et des vérifications d'intégrité en place est une bonne chose, mais les acteurs malveillants peuvent être très persistants, et aucun code figé n'est jamais impossible à rétroconcevoir, avec suffisamment de temps, d'efforts et de compétences. C'est-à-dire, à moins que nous limitions la validité du script.
Imaginez générer des milliers d'itérations uniques du même code fonctionnellement équivalent, chacune avec des fonctions de contrôle d'intégrité différentes pour chaque nouvelle version du code JavaScript. Chaque itération est uniquement utilisée et valable pendant 10 à 20 minutes, et des contrôles sont en place pour forcer le client à recharger régulièrement une nouvelle itération, rendant les anciennes itérations rapidement obsolètes et invalides.
L'objectif de cette méthode est de submerger l'acteur malveillant par la complexité et de dépasser son efficacité, afin qu'il n'ait d'autre choix que d'exécuter le JavaScript via un navigateur sans connaître la fonction réelle du code.
Rotation de champ dynamique
Le code peut être difficile à lire et à déchiffrer, mais il est souvent possible de déduire son objectif en examinant le résultat et les données collectées et envoyées. Certaines des informations envoyées au serveur peuvent être évidentes, notamment en ce qui concerne les détails tels que les caractéristiques du terminal et du navigateur.
Cependant, il serait plus difficile de déduire l'intention des fonctions qui renvoient simplement une valeur booléenne, ou d'une fonction de contrôle d'intégrité qui renvoie un entier.
Une façon de rendre la structure de la charge utile moins prévisible, et plus déroutante pour les acteurs malveillants, est de modifier les noms des champs utilisés pour rapporter chaque point de données collecté, ainsi que leur position relative dans la charge utile à chaque itération.
Comme nous l'avons vu, chaque itération JavaScript dispose d'un ensemble unique de contrôles d'intégrité du code. De plus, la charge utile utilisera des noms de champs différents, et la position d'un point de données donné changera à chaque itération.
Les noms de champ et leurs positions sont définis au moment de la compilation JavaScript, selon un algorithme prédéfini que le serveur chargé de traiter les données peut également exécuter pour retrouver, à la bonne position, les différentes informations critiques pour une détection précise des bots et des fraudes.
La figure 6 illustre la manière dont chaque champ et sa position peuvent varier d'une itération à l'autre. Les noms de champ doivent être non descriptifs pour les rendre moins évidents.
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 : Exemples d'itérations de nom de champ
Avec seulement sept champs dans la sortie (comme dans l'exemple ci-dessus), il est facile de repérer le changement d'une itération à une autre, mais imaginez ce que cela représente lorsque des centaines de points de données sont collectés et renvoyés.
Pipeline de création JavaScript et validation des données
Les différentes méthodes utilisées pour protéger le code JavaScript et assurer l'intégrité des données collectées nécessitent le développement d'un pipeline de construction et d'un processus de publication complexes. Tout d'abord, les développeurs mettent à jour le fichier JavaScript brut et bien formaté, testent la fonctionnalité et exécutent des tests de régression.
Ensuite, les développeurs utiliseront un algorithme pour générer des milliers d'itérations, qui produiront des versions uniques, chacune avec des caractéristiques différentes :
- Fonctions de contrôle d'intégrité des données qui font varier les points de données en fonction du JavaScript principal, des fonctions mathématiques ou de hachage utilisées et de leur position relative dans la logique globale
- Ensembles de codes trompeurs ou inutilisés
- Noms des champs de sortie de charge utile
- Ordre des champs de sortie de la charge utile
Une fois ces composants uniques générés, l'itération du fichier JavaScript passe par les processus suivants :
- Obscurcir le contrôle d'intégrité des données et d'autres fonctions critiques via la machine virtuelle
- Obscurcissement du code global
- Téléchargez l'itération sur le serveur Web
Une fois toutes les itérations générées et importées, le nouvel ensemble de JavaScript doit être activé en production. Cette modification est coordonnée avec le serveur qui exécute le moteur de détection des bots et des fraudes qui reçoit les données. Il doit exécuter une partie de l'algorithme utilisé dans le système du build JavaScript pour pouvoir :
- Vérifier que le client envoie la charge utile de l'itération JavaScript actuelle et non une charge obsolète
- Analyser les différents champs de la charge utile en fonction de l'itération JavaScript avec laquelle elle a été générée
- Valider les valeurs de contrôle d'intégrité du code en exécutant des fonctions équivalentes
Le produit final, avec l'obscurcissement final, doit être rigoureusement testé de bout en bout en préproduction avant le déploiement, afin de garantir que tous les composants sont synchronisés et produisent le résultat attendu. Cela nécessite la création d'un workflow de création un peu complexe pour JavaScript.
Cependant, lorsque son contenu doit être protégé contre des concurrents curieux et des acteurs malveillants, et que sa sortie a une incidence sur la sécurité des utilisateurs sur Internet et des sites Web qu'ils visitent, l'effort en vaut la peine.
Conclusion
Le code JavaScript exécuté côté client, utilisé pour collecter les empreintes digitales et la télémétrie, ainsi que la logique personnalisée conçue pour détecter les bots et les fraudes, doivent être sécurisés. Il existe plusieurs stratégies pour protéger le code et les données, mais la mise en œuvre d'une ou deux stratégies ne fournira qu'une protection marginale contre les acteurs les plus sophistiqués.
La sécurisation du code côté client et de sa charge utile nécessite une stratégie complexe reposant sur plusieurs couches de défense et technologies, notamment l'obscurcissement du code, l'insertion de code trompeur ou inutilisé, les fonctions de contrôle d'intégrité du code associées à l'obscurcissement via machine virtuelle, la randomisation de la structure de la charge utile pour la rendre moins prévisible, ainsi que la mise à jour régulière du code.
L'équation présentée à la figure 7 résume la complexité de la combinaison globale de stratégies à développer pour assurer une protection efficace.
[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 : Équation des stratégies de protection JavaScript
En fin de compte, cette combinaison oblige le client à exécuter le JavaScript, ce qui réduit le risque d'altérer les données et de neutraliser le moteur de détection. Pour limiter les efforts de développement, les solutions commerciales sont fortement recommandées pour certaines des étapes les plus complexes, telles que l'obscurcissement via machines virtuelles. Certaines stratégies, toutefois, comme les vérifications d'intégrité du code, les extraits de code trompeurs et les itérations multiples, doivent être développées et maintenues en interne afin d'assurer une protection au cas où un outil de clarification conçu par des acteurs malveillants deviendrait disponible.