不完全なURL部分文字列の検証

不完全なURL部分文字列の検証

説明

includes、indexOf、広すぎる endsWith 比較で、URLに許可ドメインが含まれるかだけを調べると、検証を回避される可能性があります。攻撃者はユーザー情報、パス、クエリ、別ドメインのホスト名に期待する文字列を入れられます。例えば https://example.com.attacker.io、http://example.com@attacker.io、https://attacker.io/?next=https://example.com です。用途に応じて、オープンリダイレクトやSSRFにつながるおそれがあります。

想定される影響

  • オープンリダイレクトを使ったフィッシングやOAuthのリダイレクトURI検証の回避により、セッションやトークンが漏えいする可能性があります。
  • サーバーから内部APIや 169.254.169.254 などのメタデータへリクエストを送られ、情報漏えいやポートスキャンにつながるおそれがあります。
  • サーバーが機密リソースへアクセスし、その内容を外部へ返す可能性があります。
  • ドメインを制限するリダイレクトやプロキシの方針を回避されるおそれがあります。

対処方法

  • URLを解析し、new URL(url).hostname を明示的な許可リストと比較してください。
  • サブドメインを許可する場合は、host === 'example.com' || host.endsWith('.example.com') で境界を確認してください。すべてのサブドメインを信頼できる場合だけ、この範囲を許可してください。単なる endsWith('example.com') は evil-example.com も通してしまいます。
  • https: などのスキームを検証し、必要に応じてポートも制限してください。
  • 可能なら、サーバーで定義した内部パスから選ばせてください。//external-host のようなネットワークパス参照は拒否してください。
  • サーバーから外部へ送る場合は、実際のIPv4・IPv6接続先について、ループバック、プライベート、リンクローカル、メタデータなどの範囲を制限してください。DNSの変更やリダイレクトによる回避も防ぎ、処理時間と応答サイズを制限してください。
  • 部分文字列や不完全な正規表現による一致を、許可の判断に使わないでください。

例

リダイレクト先のHTTPSスキームとドメイン境界を確認する例です。example.com のすべてのサブドメインを信頼する前提です。ポート方針や、サーバーからのリクエストに必要なDNS・IPの検証は別途適用してください。

変更前

javascript
const express = require('express');
const app = express();

// BAD: 部分文字列で許可を判断
app.get('/go', (req, res) => {
  const dest = req.query.dest || '/';
  // "example.com"を含む文字列を許可(危険)
  if (dest.includes('example.com')) {
    return res.redirect(dest);
  }
  res.redirect('/');
});

app.listen(3000);

変更後

javascript
const express = require('express');
const app = express();

// URLを解析し、ホスト名・ラベル境界・スキームを検証
const ALLOWED_HOST = 'example.com';

app.get('/go', (req, res) => {
  const dest = req.query.dest;
  if (!dest) return res.redirect('/');

  let parsed;
  try {
    parsed = new URL(dest);
  } catch (e) {
    return res.redirect('/'); // 無効なURLを拒否
  }

  const host = parsed.hostname.toLowerCase();
  const protocolOk = parsed.protocol === 'https:'; // ここではHTTPSを要求
  const hostOk = host === ALLOWED_HOST || host.endsWith('.' + ALLOWED_HOST);

  if (protocolOk && hostOk) {
    return res.redirect(parsed.toString());
  }
  return res.redirect('/');
});

app.listen(3000);

解説:

  • 変更前: 許可ドメインの文字列を別のホスト名、ユーザー情報、パス、クエリに入れて回避できます。この例では外部サイトへのリダイレクトにつながり、同じ検証をサーバーからのリクエストに使えばSSRFのおそれがあります。
  • 変更後: 解析したホスト名が許可ドメインまたは区切りを確認したサブドメインであり、スキームが https: の場合だけ許可します。それ以外は固定の内部パスへ戻します。

参考資料