【JS応用】Eval-計算機

Eval-計算機:JavaScriptにおける動的評価の危険性と安全な実装へのアプローチ

フロントエンド開発において、ユーザーが入力した数式を動的に評価し、計算結果を表示する「Eval-計算機」の実装は、学習の過程で一度は通る道です。しかし、JavaScriptのeval()関数を安易に使用することは、セキュリティリスクの温床となり、現代の堅牢なアプリケーション開発においては強く推奨されません。本記事では、なぜeval()が危険なのかを技術的に掘り下げ、安全かつスケーラブルな計算機を構築するための代替手法を徹底解説します。

eval関数の本質的なリスクと脆弱性

JavaScriptにおけるeval()関数は、与えられた文字列をJavaScriptコードとして実行する強力な機能です。計算機アプリにおいて、ユーザーの入力値をそのままeval()に渡すと、以下のような壊滅的なセキュリティリスクが発生します。

第一に、クロスサイトスクリプティング(XSS)の脆弱性です。攻撃者が「alert(‘hacked’)」や、悪意のある外部スクリプトを読み込むコードを入力した場合、ブラウザ上でそのコードが実行されてしまいます。これにより、セッションクッキーの盗難、ユーザー情報の改ざん、意図しないリダイレクトなどが引き起こされる可能性があります。

第二に、スコープ汚染です。eval()は実行時のスコープ内でコードを実行するため、意図しない変数の上書きや、予期せぬ挙動を誘発します。また、JavaScriptエンジンはeval()が含まれる関数に対して最適化を制限するため、パフォーマンスの低下も無視できません。最新のJavaScript環境において、eval()の使用は「コードの正当性」を証明できない限り、アンチパターンと見なされるべきです。

安全な計算機実装のためのアーキテクチャ

eval()を使わずに計算機を実現するためには、入力された文字列を「トークン(単語)」に分解し、それを「抽象構文木(AST)」として解析し、最後に計算を実行する「パーサー」を自作する必要があります。このプロセスは、コンパイラの仕組みそのものです。

基本的な実装ステップは以下の通りです。

1. 字句解析(Lexer):入力文字列を数値、演算子、括弧などのトークンに変換する。
2. 構文解析(Parser):トークンの並び順が数学的なルールに従っているかを確認し、木構造(AST)を構築する。
3. 評価(Evaluator):構築された木構造を再帰的に辿り、計算結果を算出する。

このアプローチを取ることで、ユーザーが入力できる文字を制限し(例えば数字と特定の記号のみ)、不正なコードの混入を物理的に不可能にできます。

サンプルコード:安全な数式パーサーの実装

以下に、再帰下降構文解析を用いた、eval()を使用しない安全な計算機の実装例を示します。


class Calculator {
  constructor(expression) {
    this.tokens = this.tokenize(expression);
    this.pos = 0;
  }

  tokenize(str) {
    const regex = /\d+(\.\d+)?|[+\-*/()]/g;
    return str.match(regex) || [];
  }

  peek() {
    return this.tokens[this.pos];
  }

  consume() {
    return this.tokens[this.pos++];
  }

  parseExpression() {
    let node = this.parseTerm();
    while (this.peek() === '+' || this.peek() === '-') {
      const op = this.consume();
      const right = this.parseTerm();
      node = op === '+' ? node + right : node - right;
    }
    return node;
  }

  parseTerm() {
    let node = this.parseFactor();
    while (this.peek() === '*' || this.peek() === '/') {
      const op = this.consume();
      const right = this.parseFactor();
      node = op === '*' ? node * right : node / right;
    }
    return node;
  }

  parseFactor() {
    const token = this.consume();
    if (token === '(') {
      const node = this.parseExpression();
      this.consume(); // ')' を消費
      return node;
    }
    return parseFloat(token);
  }

  calculate() {
    try {
      return this.parseExpression();
    } catch (e) {
      return "Error";
    }
  }
}

// 使用例
const calc = new Calculator("10 + 2 * (5 - 3)");
console.log(calc.calculate()); // 出力: 14

このコードでは、正規表現を用いて入力をトークン化し、演算子の優先順位(乗除算が加減算より先であること)を考慮して再帰的に処理しています。これにより、eval()を一切使わずに安全かつ正確な計算が可能です。

実務におけるエンジニアリングアドバイス

実務で計算機機能を実装する際、上記の自作パーサーはシンプルで非常に強力ですが、複雑な関数(sin, cos, logなど)や変数管理が必要な場合は、自作のコストが膨大になります。その場合、以下の戦略を検討してください。

1. 信頼できるライブラリの利用:
Math.jsのような、実績のある数式評価ライブラリを使用してください。これらのライブラリは、セキュリティを考慮したパーサーを内蔵しており、eval()の脆弱性を回避しつつ、高度な計算機能を安全に提供します。

2. Web Workerの活用:
もし計算処理が非常に重い場合、メインスレッドをブロックしないようWeb Workerで処理を行うべきです。UIの応答性を維持しつつ、計算のボトルネックを解消できます。

3. 入力値のバリデーション:
どのような手法を用いるにしても、ユーザーの入力値に対するバリデーションは必須です。正規表現を用いて、許可されていない文字(英字や特殊記号)が含まれていないか、事前にチェックを行いましょう。

4. エラーハンドリングの徹底:
ユーザーが「5 + * 2」のような無効な数式を入力した際、アプリがクラッシュしないように例外処理を実装してください。具体的には、パーサーからエラーを投げ、ユーザーに対して「無効な数式です」と分かりやすいフィードバックを表示することが、UX向上の鍵となります。

まとめ

「Eval-計算機」の実装は、フロントエンド開発におけるセキュリティ意識を試す絶好の機会です。eval()関数は即席で機能を実現する魔法のように見えますが、その代償はあまりに大きく、現代のエンジニアとしては決して選択すべきではありません。

今回紹介した再帰下降構文解析の手法は、計算機だけでなく、DSL(ドメイン固有言語)の設計や、複雑なデータ構造の解析にも応用できる極めて強力なスキルです。自らパーサーを構築することで、JavaScriptの内部挙動に対する理解が深まり、より安全で信頼性の高いコードを書く力が養われます。

「動くものを作る」だけでなく「堅牢なものを作る」ことこそが、プロフェッショナルなフロントエンド・スペシャリストの使命です。今回解説した手法を参考に、ぜひあなた自身のプロジェクトで、安全な数式評価エンジンを実装してみてください。技術的な誠実さが、長期的なメンテナンス性とユーザーの信頼を勝ち取る唯一の道です。

コメント

タイトルとURLをコピーしました