🗒️

JavaScript の危険な eval を回避する方法まとめ

2022/07/31に公開

はじめに

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 を弱めた構文のパーサーは、多く公開されていそうですが、その中でも簡単に導入できそうなものを紹介します。他にもあればお知らせください。以後説明するものより比較的安全性は高いと思いますが、ライブラリのスター数が少ないので◯としました。

https://github.com/justinfagnani/jexpr
https://github.com/shepherdwind/simple-evaluate

[△] Node.js VM や JSランタイムをもつライブラリを使う

何らかの事情や、プロトタイプさえできれば良いという理由で、パーサーなどの実装は避けたいということもあるでしょう。そうしたときに、evalを使う代わりにすべきことを、以下に書きたいと思います。これから先の全てのものが△なのは、多かれ少なかれ脆弱性を持っているため、本番環境での利用は非推奨となるためです。

Node.js の VM を使う方法をご紹介したいところなのですが、これについては筆者は詳しくないので、そういう方法もあるといったイメージで読んでいただければと思います。 VM は、Node.js を利用していればrequire('vm')で利用することが出来ます。

APIは、https://nodejs.org/api/vm.html にありますが、ここにも書かれている通り、

だそうです。

ライブラリ safe-eval について

https://github.com/hacksparrow/safe-eval
safe-eval は VM を利用したライブラリです。そのメリット・デメリットをお伝えしたいと思います。メリットは、VMを利用していることにより、あとで説明する Functionより安全だと考えられる点です。デメリットは、このライブラリのメンテナンスが4年前から途絶えている点と、この README に書かれている通り脆弱性が発見されている点です。
例えば、 https://github.com/hacksparrow/safe-eval/issues/12 に書かれている通り グローバルなObject.constructor に破壊的な変更を加えてしまうようです。その他の issue もチェックする必要があると思われます。


その他のライブラリについて

その他については、自分で軽く調査した程度なので、参考までにご紹介します。安全性の判断は特に行いません。

https://github.com/maple3142/wasm-jseval
こちらは、QuickJS は C/C++ に組み込める軽量な JavaScript エンジンである QuickJS を用いて WASM を介して eval を実装したようなものになっていて、スター数は比較的多めです。安全性は自分からはわからない (GitHub issue が少ないのは気になります) のと、ライブラリとしてはそこそこ大きなものになるなという印象を受けました。


https://github.com/espadrine/localeval
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を用いるコードでは、以下のlooseJsonParseeval の役割を果たします。なおかつ、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