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