JavaScript | eval() を使わずに安全に同じことをする方法

JavaScript
スポンサーリンク

eval() を使わずに 安全に・再現可能に・制御された形で動的な振る舞いを実現する方法 を、実用的なパターンとサンプルコードで見せます。初心者でも試せるよう、短く分かりやすい例を中心にします。

まとめ(先に結論)

安全に動的振る舞いを実現する代表的な方法は主に次の4つ:

  1. 関数レジストリ(キーで関数を選ぶ) — 最も簡単で安全。
  2. ミニ DSL(JSONなどで動作を記述して専用の「実行器」で解釈) — 柔軟で安全。
  3. シリアライズ+ファクトリ(状態を別に保存して関数を復元) — クロージャの代替。
  4. 厳格なホワイトリストを使った new Function/解析 — 最終手段、注意が必要。

以降、コード例つきで説明します。


1) 関数レジストリ(推奨)

「ユーザーから来る値は関数名(キー)のみ。実際の関数はあらかじめ登録しておき選んで呼ぶ」方法です。eval をまったく使わず安全です。

// 関数レジストリを作る
const registry = {
  add: (a, b) => a + b,
  sub: (a, b) => a - b,
  greet: (name) => `Hello ${name}`
};

// 例:外部入力(安全なのはキーだけ)
// 外部から来るのは "action"(文字列) と args(配列)
function handleRequest(action, args) {
  const fn = registry[action];
  if (typeof fn !== 'function') {
    throw new Error('許可されていない操作です');
  }
  return fn(...args);
}

// 利用例
console.log(handleRequest('add', [5, 3])); // 8
console.log(handleRequest('greet', ['Halu'])); // "Hello Halu"
JavaScript

利点:実装が簡単、ホワイトリスト化され安全。
欠点:事前に用意した操作しかできない(ただしこれが安全の源泉)。


2) ミニ DSL(JSON で動作を記述 → 安全なインタプリタで実行)

ユーザー入力は「やること」の記述(JSON)だけ。実行は自前の解釈器(interpreter)で行います。演算、条件、ループなど必要最小限を定義しておけば eval は不要。

例:簡単な算術 DSL を作る(安全に式を評価する)

// DSL 例: { op: "add", args: [1, {op:"mul", args:[2,3]}] } -> 1 + (2*3)
function run(node) {
  if (typeof node === 'number') return node;
  if (typeof node === 'string') return node; // ※文字列は別扱い
  switch(node.op) {
    case 'add': return run(node.args[0]) + run(node.args[1]);
    case 'sub': return run(node.args[0]) - run(node.args[1]);
    case 'mul': return run(node.args[0]) * run(node.args[1]);
    case 'div': return run(node.args[0]) / run(node.args[1]);
    default: throw new Error('未対応の操作:' + node.op);
  }
}

// 利用
const expr = { op: 'add', args: [1, { op: 'mul', args: [2, 3] }] };
console.log(run(expr)); // 7
JavaScript

利点:柔軟で安全に拡張可能。外部から送られた JSON をそのまま評価しても安全。
欠点:インタプリタを設計する手間がある(でもセキュリティの投資として価値大)。


3) シリアライズ + ファクトリ(クロージャの代替)

クロージャ(スコープに閉じた変数)をそのまま toString() で保存しても復元できません。代わりに「状態(データ)」と「どのファクトリ関数で復元するか」を保存する方法です。

// factory は状態を受け取って関数を返す
function makeAdder(base) {
  return function(num) { return base + num; };
}

// シリアライズする(状態だけ保存)
const state = { kind: 'adder', base: 10 }; // ←外部に保存可能(JSON化)
const factoryRegistry = {
  adder: (s) => makeAdder(s.base)
};

// 復元
const restored = factoryRegistry[state.kind](state);
console.log(restored(5)); // 15
JavaScript

ここでは toString() は使わない。状態(base: 10)を保存し、どの工場(adder)で復元するかを指定するだけです。

利点:クロージャの状態を安全に永続化/復元できる。
欠点:状態と工場(factory)を用意する必要があるが、安全第一。


4) ホワイトリストを使った限定的な new Function(要注意)

どうしても文字列から関数を作る必要がある場合、非常に慎重にホワイトリストや静的解析を入れて制限します。推奨しませんが、現実的な妥協策です。

ポイント:

  • 許可されるコードパターンを限定(例:単純な式のみ、API 呼び出し禁止)
  • 必ず正規表現やパーサで検査して安全でなければ拒否
  • サンドボックス化(別プロセス・Worker)で実行することを検討

簡単な検査例(ただし正規表現だけで完全に安全にするのは難しい):

const allowedPattern = /^[0-9+\-*/\s()a-zA-Z,]+$/;
function safeCreate(argNames, body) {
  if (!allowedPattern.test(body)) throw new Error('不許可の文字が含まれる');
  return new Function(...argNames, body);
}
JavaScript

注意:これは例示用。現実にはもっと厳格なパーシングが必要です。


例:先ほどのクロージャデモを「安全に」置き換える

先のデモでは add10.toString()eval で復元して失敗しました。状態を保存してファクトリで復元する例:

// 以前: const code = add10.toString(); // NG

// 新しい安全設計:状態を保存してファクトリで復元
function makeAdder(base) { return function(num){ return base + num; }; }

// 状態として保存
const saved = { type: 'adder', base: 10 }; // JSON.stringify(saved) で保存可能

// 復元ロジック(ファクトリレジストリ)
const factories = {
  adder: s => makeAdder(s.base)
};

const restored = factories[saved.type](saved);
console.log(restored(5)); // 15 ← 正しく再現できる
JavaScript

実務でのベストプラクティス

  • ユーザー入力は「コード」ではなく「データ(キー / パラメータ / JSON)」で受ける。
  • 実行するロジックはサーバー側か事前登録された関数(レジストリ)で安全に管理。
  • どうしても文字列評価が要る場合は厳重に検査・サンドボックス化する(そしてその必要性を再検討する)。
タイトルとURLをコピーしました