JavaScriptで比較的安全にeval()する


evalって?

文字列をJavaScriptのコードとして評価してくれます。
一見便利だけど、めっちゃ危険だから通常使うことはありません。
使ったらESLintにも怒られます。

でも、外部からコードを注入できるのはとても拡張性が高いので、クローズドなBotとかの開発に使いたい場面はあります。

eval的なやつ

eval()が一番有名だと思うけど、実は他にも同じようなことができる方法があります。
具体的には、グローバルオブジェクトのFunction()と、Node.jsのVMです。

安全性としては、たぶんeval() <<< Function() << Node.js VMなんだけれど、VMはちょっと敷居が高そうだったので、この記事ではFunction()を使った方法について紹介します。

ちなみに、npmにsafe-evalっていうNode.js VMのラッパーがあったので、使ってみてもいいかも。

Function()の使い方

基本はeval()と同じです。

const greeting = Function('console.log("hello.")')
greeting() // hello.

けど、ローカルスコープに閉じ込められてるので、eval()と比べるとかなり安全。
実行するコードを渡すとこんな感じ。

const runCode = (code) => Function(code)()
const code = 'return new Date()'
console.log(runCode(code)) // 2021-07-21T16:41:19.389Z

ただし、ローカルスコープで評価されているので、グローバルに定義した関数はもちろん呼び出せない。
以下のコードはエラーになります。

const greeting = (name) => `hello, ${name}.`
const runCode = (code) => Function(code)()
const code = 'return greeting("michinosuke")'
console.log(runCode(code)) // greeting is not defined

じゃあどうするかというと、ちょっと複雑だけど、こんな感じにします。

const greeting = (name) => `hello, ${name}.`
const runCode = (code) => Function(`return (greeting) => {${code}}`)()(greeting)
const code = 'return greeting("michinosuke")'
console.log(runCode(code)) // hello, michinosuke.

awaitするコードを実行したいときは次のようにかきます。

const runCode = (code) => Function(`return async (fetch) => {${code}}`)()(fetch)

const runCodeAndPrint = async (code) => {
  console.log(await runCode(code))
}

runCodeAndPrint(`
const html = await fetch("https://example.com/").then(res => res.text())
return html.match(/<title>.+/)[0]
`) // <title>Example Domain</title>

非同期なFunction()

一応、非同期関数もコンストラクタで生成できます。

const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
AsyncFunction('return "async hello"')().then(str => console.log(str))

AsyncFunction

まとめ

紹介したFunction()は、ローカルスコープで評価されるとはいえ、悪意のある人物なら容易に悪用できるので、不特定多数に公開するサービスでの使用はやめた方がいいと思います。