DependabotAlert-loadash-CVE-2021-23337
2021年5月29日
5/17に報告された「lodash」のDependabot Alertのメモ。
この脆弱性はv4.17.21より前のバージョンにて発生している。修正バージョンはすでに公開されており、v4.17.21にアップグレードすることが推奨されている。
カテゴリは「high severity」。記事執筆時点での最新バージョンはv4.17.21。
GitHub Advisory Database番号は「CVE-2021-23337」。
lodashとは
lodashはJavaScriptの便利関数がたくさん詰まっているライブラリ。私はあまり直接使ったことがないのだが、依存パッケージの依存パッケージになっていることが多く、なんだかんだいろいろなツールで使われている印象がある。
こういう便利関数は、多くの経験にもとづいたベストプラクティスが詰まっているものだとは思うけど、言語自体がアップデートされて似たような機能が標準装備されることもあり、それで十分になることが多い。
あと他人の経験が自分の経験にそのまま活きるかというとそれはケースバイケースなわけで、思考停止状態に陥らないためにも無闇やたらに使わないようにしている。
lodashには11つのカテゴリに分けられた機能が存在する。せっかくなのでサンプルコードと一緒に少しピックアップしてみようかな。脆弱性については最後に記す。
Arrayカテゴリ
_.difference([2, 1], [2, 3]); // [1]
difference
は二つの配列を比較して、一つ目の配列にしか含まれていない値の配列を返す。便利だな。
Collectionカテゴリ
_.sample([1, 2, 3, 4]);
sample
は受け取った配列に含まれる値の中からランダムにピックアップして返してくれる。データをサンプリングする時とかに使える。便利だな。
Dateカテゴリ
_.now();
Dateカテゴリにはこのnow
しかない。タイムスタンプをミリ秒で返す。Date.now()
との違いはない。そのものらしい。
Functionカテゴリ
function isEven(n) {
return n % 2 == 0;
}
_.filter([1, 2, 3, 4, 5, 6], _.negate(isEven));
// => [1, 3, 5]
説明が難しいのだが、_.nagate
はある関数とは反対の結果を返す関数を生成するもの。実装を見た感じ、元の関数は引数3つまでしか受け取れないっぽい。ノーコメント。
Langカテゴリ
_.isEmpty(value);
isEmpty
は引数に渡されたオブジェクト、コレクション、マップ、セットが空かどうかを判定する。意外とめんどいんだよね。
isなんとか、toなんとか系の関数がLangカテゴリに属しているのだけど、なんでLangという名前なんだろう。
Mathカテゴリ
_.mean([4, 2, 8, 6]);
mean
は渡された数値配列の平均を計算する。言語標準にありそうで無い実装。
「average」という名前の方が一般的な気がしていたけど統計学の世界では「平均値」のみを指す場合は「mean」のほうが正らしい。(averageには中央値とかも含まれる)
Numberカテゴリ
_.inRange(3, 2, 4);
inRange
は特定の数値が、特定の範囲内に含まれるかどうかを判定する。
判定する値は第一引数なのか、第三引数なのかとか、同値は含まれるのかとかが若干わかりづらいなと思った。
Objectカテゴリ
var object = { 'a': 1, 'b': '2', 'c': 3 };
_.pick(object, ['a', 'c']);
// => { 'a': 1, 'c': 3 }
とあるオブジェクトから指定したキーのものを取り出して新しいオブジェクトを作る。便利だな。
関係ないけどサンプルコードの2だけ文字列なのが気になる。
Seqカテゴリ
var wrapped = _([1, 2, 3]);
// Returns an unwrapped value.
wrapped.reduce(_.add);
// => 6
_
はlodashオブジェクトを生成する基本関数。この関数により、プリミティブなオブジェクトがラップされ、lodashのメソッドが備わっているように扱える。
Stringカテゴリ
var compiled = _.template('hello <%= user %>!');
compiled({ 'user': 'fred' }); // => 'hello fred!'
template
はその名の通りテンプレート関数を生成する。<%=
と%>
で囲った文字列が変数になり、引数にその名前をキーに持つデータを渡すとコンパイルされた文字列を取得できる。
これは今回の本題になる関数。
Utilカテゴリ
var func = _.over([Math.max, Math.min]);
func(1, 2, 3, 4);
// => [4, 1]
over
は引数に渡した関数を、順に実行した結果を配列で返す関数を作る。
脆弱性について
本題。
lodashに存在していた脆弱性は「Command Injection」と呼ばれるもの。コマンドインジェクションはは任意の文字列が意図せずJavaScriptとして実行されてしまうことにより、意図しないコマンドがプログラムに注入されてしまうことを指す。
template
関数は第一引数にテンプレート文字列、第二引数にオプションを受け取る。第二引数のオプションの一つにvariable
という設定がある。
このオプションはtemplate
の実行によって生成された関数の引数の名前をセットするためのもの。variable
を使う場合のtemplate
の使い方は以下のような感じ。
const complied = _.template('hello <%= custom.name %>!', { variable: 'custom' });
console.log(complied({ name: 'world' }));
complied
関数は以下のようにcustom
と言う名前の引数をとる関数になる。これは指定した文字列がそのまま関数の一部として取り込まれているということになる。
function (custom) {
// テンプレート文字列と`custom`をコンパイルする処理
}
v4.17.21時点でのtemplate関数の実装を見てもらうと分かりやすいと思う。
ちなみにvariable
オプションに何も設定しない場合、lodashは内部的にobj
という文字列を設定する。つまり以下のように扱われる。
const complied = _.template('hello <%= obj.name %>!');
本来であれば、変数化したい部分にはobj.
のようにコンテキストを与える必要があるが、実際にtemplate
を使う場合はobj.
の部分を書くことはないだろう。
const complied = _.template('hello <%= name %>!');
このままだとname
という変数は定義されていないため、リファレンスエラーになるはずだが、エラーにならないのはlodashが内部でwith
文を挿入しているからだった。
ここまでが今回の脆弱性を理解するために必要な前提知識となる。Snykに報告されているPoCは以下のようなもの。
_.template('', { variable: '){console.log(process.env)}; with(obj' })()
これを実行するとlodash内部では、以下のようなコードが出来上がる。(わかりやすいように改行を入れている)
function (){
console.log(process.env)
};
with(obj) {
// テンプレート文字列と`custom`をコンパイルする処理
}
結果として得られるcompile
関数はvariable
として受け取った文字列に含まれる処理を実行する関数となってしまい、コマンドインジェクションが成立する。
修正後の実装にはvariable
の値をテストする処理が追加されていた。(テスト用の正規表現)
文字列を関数化するのは、強力な機能ではあるがなんでもできるが故に脆弱性を生みやすい気がするので、気をつけたい。
余談
関係ないけど、lodashはメソッド単体を扱うのではなく、シーケンスをイメージしながら複数のメソッドを組み合わせて使うことで効果を発揮する感じっぽいのね。
便利だと思うけど、個人的に関数のインターフェースとか命名の仕方(less thanをltに略すとか)とかにあまり共感できなかったので、多分今後も使わないかなぁと思う。