JavaScript の危険な eval を回避する方法まとめ
はじめに
JavaScriptにおいて与えられたstringをJavaScriptとして評価するeval
関数をご存じでしょうか。便利な関数ではあるのですが、任意のJavaScriptを実行できるという強大すぎる能力のせいで、同時にセキュリティ上避けるべき関数としても知られています。
本記事では、eval
の代替となる方法をいくつかご紹介します。これを読めば、eval
を使う必要が全くないということがわかり、ベストな方法での実装をすることができると思います。
[◎] 他の安全な関数を使うか自分で実装する
まず、最初に検討すべきなのは、eval
や後で紹介する Function
といったようなJavaScript実行系のものを使わないことです。
多くのケースでは、JavaScript を直接実行しない方法に書き換えられると思います。JSON やYAML などとして解釈できるものであれば、JSON.parse
などの専用の関数を用いればいいですし、他の変数に影響を与えるようなコードであれば、関数の返り値として解釈するのが望ましいでしょう。
具体的なコード
例えば
eval(`{
a: apple
}`)
といったようなコードであれば、
JSON.parse(`{
"a": "apple"
}`)
と JSON のパースの形にしてもいいですし、Json5やYAMLとみなせば、js-yaml
などのライブラリを用いて
import Json5 from 'json5'
import jsyaml from 'js-yaml'
JSON5.parse(`{
a: apple
}`)
jsyaml.load(`{
a: apple
}`)
としてもいいでしょう(js-yaml
を用いると任意のYAMLを受け入れることになるため、ここで推奨されるかは微妙ですが)。
let x
eval(`
x = { a: apple }
`)
であれば、正規表現やパーサーで x = hogehoge
のhogehogeの部分を抜き出して、JSON.parse
し、xに代入するという関数を書くのが望ましいです。
他には、四則演算や論理演算をしたいとか、簡単なプログラミング言語としたいが、実装が大変で eval
を使いたいというニーズもあるとは思います(この記事を書いたきっかけもそうでした)。そうした場合も、できる限りは構文を解析するパーサーを実装するのが望ましいです。
eval('1 + 2 / (33 % 4) * 5') // eval の代わりに
eval('a == 1 && b != 4') // パーサーを用いるのが望ましい
[⚪︎] パーサーライブラリ
JS を弱めた構文のパーサーは、多く公開されていそうですが、その中でも簡単に導入できそうなものを紹介します。他にもあればお知らせください。以後説明するものより比較的安全性は高いと思いますが、ライブラリのスター数が少ないので◯としました。
[△] Node.js VM や JSランタイムをもつライブラリを使う
何らかの事情や、プロトタイプさえできれば良いという理由で、パーサーなどの実装は避けたいということもあるでしょう。そうしたときに、eval
を使う代わりにすべきことを、以下に書きたいと思います。これから先の全てのものが△なのは、多かれ少なかれ脆弱性を持っているため、本番環境での利用は非推奨となるためです。
Node.js の VM を使う方法をご紹介したいところなのですが、これについては筆者は詳しくないので、そういう方法もあるといったイメージで読んでいただければと思います。 VM は、Node.js を利用していればrequire('vm')
で利用することが出来ます。
APIは、https://nodejs.org/api/vm.html にありますが、ここにも書かれている通り、
だそうです。
safe-eval
について
ライブラリ safe-eval
は VM を利用したライブラリです。そのメリット・デメリットをお伝えしたいと思います。メリットは、VMを利用していることにより、あとで説明する Function
より安全だと考えられる点です。デメリットは、このライブラリのメンテナンスが4年前から途絶えている点と、この README に書かれている通り脆弱性が発見されている点です。
例えば、 https://github.com/hacksparrow/safe-eval/issues/12 に書かれている通り グローバルなObject.constructor
に破壊的な変更を加えてしまうようです。その他の issue もチェックする必要があると思われます。
その他のライブラリについて
その他については、自分で軽く調査した程度なので、参考までにご紹介します。安全性の判断は特に行いません。
QuickJS を用いて WASM を介して eval を実装したようなものになっていて、スター数は比較的多めです。安全性は自分からはわからない (GitHub issue が少ないのは気になります) のと、ライブラリとしてはそこそこ大きなものになるなという印象を受けました。
こちらは、QuickJS は C/C++ に組み込める軽量な JavaScript エンジンであるREADME が自らの脆弱性の範囲を説明していて、信頼できます。利用時には、それ以外の脆弱性を持つ可能性があることを忘れてはいけません。
[△] Function を用いる
ライブラリを導入したくないし、Node.js の VM も API を調べるほどではない、あるいは Node.js を利用できないという場合は、eval
より安全でeval
と同等の機能が実現できるFunction
を用いることになるでしょう。つまり、eval
を使用するくらいなら、Function
を使ったほうがまだマシということになります。基本的には以下の記事のとおりです。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/eval#eval_を使わないでください!
ここから引用すると
eval() は呼び出し元の権限で渡されたコードを実行する危険な関数です。悪意のある第三者に影響を受ける可能性のある文字列で eval() を実行すると、そのウェブページや拡張機能の権限において、ユーザーのマシン上で悪意のあるコードを実行してしまう可能性があります。さらに重要なことに、サードパーティのコードは eval() が呼び出されたスコープを見ることができるので、類似の Function では影響を受けない方法でも攻撃を受ける可能性があります。
とのことです。以下は引用したコードなりますが、Function
を用いるコードでは、以下のlooseJsonParse
が eval
の役割を果たします。なおかつ、looseJsonParse
の引数の文字列中の Date
は近くのjavascriptによって上書きされたDate
が用いられることはなく、window.Date
が使われることが保証されます (詳細は元記事を参照してください)。
外部の JavaScript に依存しないことで、パフォーマンスの面でも優れていると指摘されています。
function looseJsonParse(obj) {
return Function('"use strict";return (' + obj + ')')();
}
const obj = looseJsonParse(
"{a:(4-1), b:function(){}, c:new Date()}"
)
const f = looseJsonParse(`function () {
return Math.random() * 3;
}`)
[△] 正規表現で危険な文字列を排除する
これは、上記の方法と組み合わせるものですが、正規表現で受け取る文字列で制限をかけることが可能です。
たとえば、電卓のような用途では、正規表現
/[ -()0-9/*+.]*/
にマッチするかを検証、あるいは、それ以外の文字を除くことで、(1+2.0) * 3
などの実行が安全な式のみにすることができます。しかしながら、floor
関数などの関数を追加しようと思ったときなどに、毎回正規表現を更新する必要があり、その間に悪意のあるコードを許すような正規表現にしてしまう可能性があります。メンテナンスを必要とする用途で、正規表現による方法に頼り続けるのは、大変だと思われます。
最後に
以上、eval
の代替になる方法を説明いたしました。
Function
という代替手段がある以上、基本的にeval
を使うことはないと思います。
理想的には、Function
も用いずに、用途に合わせた安全な関数を用いるか、パーサーを自前で実装するかの2択が好ましいでしょう。
自分のやりたいことに合わせ、適切な方法を選んでください。
Discussion