危険な動的メソッドアクセス

危険な動的メソッドアクセスによるコードインジェクション

説明

ユーザー入力で window、globalThis、global の関数名を選ぶと、意図しない関数を呼び出す可能性があります。eval は渡された文字列を実行し、Function は後で呼び出せる関数を生成します。実行する文字列も攻撃者が指定できる場合、その環境の権限で任意のコードを実行されるおそれがあります。

想定される影響

  • コードを実行する関数を介した任意のJavaScriptの実行
  • アクセス可能なCookie、トークン、環境変数の漏えい
  • アプリケーションの状態の変更や、権限のある操作の悪用
  • ループや重い計算によるリソースの枯渇

対処方法

  • ユーザー入力でグローバルオブジェクトや関数オブジェクトのメソッドを動的に選ばせないでください。
  • 専用のオブジェクトや Map に許可した関数だけを登録し、Object.hasOwn などでキーを検証してから呼び出してください。
  • グローバルへのアクセスが必要でも、encodeURIComponent や decodeURIComponent などに固定の許可リストを絞ってください。
  • 許可リストにない名前は拒否し、安全な既定の動作を使ってください。
  • eval、Function、setTimeout、setInterval、execScript など、コード生成や文字列実行が可能な関数は許可しないでください。JSONの解析、明示的な分岐、専用APIの対応表などを使ってください。
  • 追加の対策として、ブラウザーのCSPでは 'unsafe-eval' を避けてください。Node.jsでは互換性を確認し、--disallow-code-generation-from-strings や実行・ビルド時のポリシーで文字列からのコード生成を制限してください。

例

変更前

javascript
// ブラウザーの例
// URLフラグメントをグローバル関数名として使います。
// https://site.example/#evalでwindow["eval"]("alert('pwned')")を呼び出します。
window.addEventListener('load', () => {
  const name = location.hash.slice(1); // ユーザー指定
  window[name]("alert('pwned')"); // eval/Functionを呼び出せます。
});

// Node.js(Express)の例
const express = require('express');
const app = express();
app.get('/do', (req, res) => {
  const action = req.query.action; // ユーザー指定
  // /do?action=evalで例の文字列が実行され、グローバル状態が変わります。
  globalThis[action]("globalThis.exampleResult = 1"); // 脆弱です。
  res.send('done');
});

変更後

javascript
// 専用APIオブジェクト、許可リスト、キー検証で呼び出し先を制限します。
// Object.create(null)で継承されたメンバーを除きます。
const API = Object.create(null);
API.ping = (d) => 'pong';
API.upper = (d) => String(d || '').toUpperCase();

function callApi(name, data){
  // 独自のキーの存在と関数の型を確認します。
  if (!Object.hasOwn(API, name) || typeof API[name] !== 'function') {
    throw new Error('invalid action');
  }
  return API[name](data);
}

// ブラウザーの例
window.addEventListener('load', () => {
  const name = new URL(location.href).searchParams.get('action');
  try {
    const result = callApi(name, 'hello');
    console.log(result);
  } catch (e) {
    console.warn('blocked');
  }
});

// Node.js(Express)の例
const express = require('express');
const app = express();
app.get('/do', (req, res) => {
  try {
    const result = callApi(req.query.action, req.query.data);
    res.json({ ok: true, result });
  } catch {
    res.status(400).json({ ok: false, error: 'invalid action' });
  }
});

// コード実行関数を除いた、限定的なグローバル関数の許可リストです。
const SAFE_GLOBALS = Object.freeze(["encodeURIComponent", "decodeURIComponent"]);

app.get('/encode', (req, res) => {
  const action = req.query.action;
  if (!SAFE_GLOBALS.includes(action)) {
    return res.status(400).send('invalid action');
  }
  res.send(globalThis[action](String(req.query.value || '')));
});

説明:

  • 変更前: ユーザー入力でグローバル関数を選びます。eval を選ぶと例の固定文字列が実行されます。Function は関数を生成するだけで、本文は生成された関数を呼び出したときに実行されます。実行文字列も外部から指定できれば、任意のコード実行につながるおそれがあります。
  • 変更後: 許可した関数だけを登録し、独自のキーと関数の型を検証して、不明な名前を拒否します。プロトタイプのないオブジェクトで継承メンバーを避けます。グローバル関数の許可リストが必要な場合も、コード生成関数は除外してください。

参考資料