[Astro] #150 CORS Bulk Diagnostics & Inspector — ブラウザ直接実行型CORSチェッカーの実装とフロントエンド完結の設計思想

[Astro] #150 CORS Bulk Diagnostics & Inspector — ブラウザ直接実行型CORSチェッカーの実装とフロントエンド完結の設計思想

はじめに

フロントエンド開発や外部API連携において、誰もが一度はハマるのが CORS(Cross-Origin Resource Sharing)ポリシー の問題です。

ネット上には多くのCORS診断ツールが存在しますが、その多くは「自前のバックエンドサーバー(プロキシ)を経由してAPIを叩く」仕組みになっています。この方式だと、相手先サーバーのCloudflareやBot対策フィルターに引っかかり、「実際にはCORS許可されているのに BLOCKED になる」という偽陰性(False Negative) が多発します。

そこで今回、クライアントのブラウザから直接 fetch() を投げることで、ユーザーの閲覧環境における100%正確なCORS挙動 を判定できる「CORS Bulk Diagnostics Tool」を構築・移植しました。

モジュール内容
BULK DIAGNOSTICS複数URL(改行区切り)の一括CORS判定。応答速度(Latency)とHTTPステータス取得
RATE LIMIT & MASTER KEYクライアントサイドでのレート制御(300msディレイ)と Web Crypto API (SHA-256) による無制限モード
HEADER INSPECTORレスポンスヘッダーの解析(Allow-Origin, Allow-Methods, Allow-Headers, Allow-Credentials 等)
CORS FIX GENERATORバックエンド側でCORSを許可するための設定コード自動生成(Express, Cloudflare Workers, Nginx)
PROTOCOL.LAIN UIレトロフューチャー&サイバーパンク風の3パネルUIレイアウト

スクリーンショット

起動画面

[Astro] #149 CORS Bulk Diagnostics & Inspector — ブラウザ直接実行型CORSチェッカーの実装とフロントエンド完結の設計思想

スキャン中

[Astro] #149 CORS Bulk Diagnostics & Inspector — ブラウザ直接実行型CORSチェッカーの実装とフロントエンド完結の設計思想

リサルト画面

[Astro] #149 CORS Bulk Diagnostics & Inspector — ブラウザ直接実行型CORSチェッカーの実装とフロントエンド完結の設計思想

動画(GIF)

[Astro] #149 CORS Bulk Diagnostics & Inspector — ブラウザ直接実行型CORSチェッカーの実装とフロントエンド完結の設計思想

1. 技術スタック

ライブラリ / API役割
Astro 5ページフレームワーク
Fetch API (mode: 'cors')ブラウザ標準のCORS通信・疎通判定
Web Crypto API (subtle.digest)クライアントサイドでのマスターキーハッシュ照合(SHA-256)
Vanilla JavaScript非同期並列/直列リクエスト制御・DOM動的描画

2. なぜ「ブラウザ直接実行型」なのか(Proxy型の罠)

従来のWebベースCORSチェッカーの多くは、次のようなシーケンスで診断を行います。


[ブラウザ] ──(1) リクエスト──> [チェッカーのサーバー] ──(2) Proxyリクエスト──> [対象API]

この構成には大きな弱点が2つあります。

  1. IP / Origin の相違: 対象APIが特定のOriginやIPゾーンのみ許可している場合、チェッカーのサーバーIPからのリクエストは拒否されてしまいます。
  2. Bot対策(Cloudflare等)の誤発動: サーバーからの機械的なリクエストはWAFにブロックされやすく、CORS設定以前の問題でエラーになります。

本ツールでは、あえてサーバーを一切挟まず、ユーザーのブラウザから直接ターゲットへ fetch() を発行する 構成を採りました。

const fetchOpts = {
  method: method,
  mode: 'cors', // CORSモードを明示
  credentials: includeCredentials ? 'include' : 'same-origin'
};

try {
  const res = await fetch(url, fetchOpts);
  // CORS通過 (ALLOWED)
} catch (err) {
  // CORS違反またはネットワークエラー (BLOCKED)
}

ブラウザ自身のセキュリティ機構を利用するため、「今動かしているこのブラウザ環境からAPIが叩けるか」を100%正確に再現できます。

3. ブラウザのDevToolsコンソールエラーとアプリ挙動の関係

直接 fetch() を投げた際、CORS違反が起きるとブラウザのコンソールに次のような赤文字エラーが出力されます。

Access to fetch at '[https://duckduckgo.com/](https://duckduckgo.com/)' from origin 'http://localhost:4321'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

一見するとアプリ側のバグのように見えますが、これはブラウザ(Chromium / Firefox等)の仕様上、JavaScriptの catch 処理に渡る手前でDevToolsへ強制出力されるログです。

アプリ側のコードでは try...catch でこの例外を正常にキャッチし、画面上に BLOCKED として視覚化できているため、このコンソールエラーが出力されること自体が「正確にCORS拒否を検知できた証拠」となります。

4. クライアントサイド完結認証と設計上の割り切り

一括診断機能(Bulk Check)を公開するにあたり、対象APIへのDoS攻撃化を防ぐため、デフォルトで「最大5件まで」「リクエスト間に300msのディレイ」というレート制限を設けています。

一方で、開発者自身が大量のテストを行うために Web Crypto API (SHA-256) を用いた Master Key 認証 を実装しました。

// パスワード文字列の SHA-256 ハッシュ化
async function sha256(str) {
  const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
  return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
}

btnAuthKey.addEventListener('click', async () => {
  const hash = await sha256(inputMasterKey.value.trim());
  if (hash === MASTER_KEY_HASH) {
    isMasterUnlocked = true; // ディレイ解除・制限緩和
  }
});

フロントエンド完結認証のトレードオフ

当然ながら、JavaScriptのみで完結しているため、開発者ツールから isMasterUnlocked = true と入力すれば誰でもバイパス可能です。

しかし、以下の理由からこの割り切りが最適解であると判断しました。

  • サーバー代・管理コストゼロ: バックエンドを持たない静的配信(Cloudflare Pages / Astro)で運用できる。
  • 被害半径がゼロ: サーバー側に秘密のAPIキーやクレデンシャルを保持していないため、ハックされても奪われる資産や増大する費用が存在しない。
  • 負荷の自責化: レート制限を無視して大量リクエストを連打したとしても、帯域とCPUを消費するのは「実行したユーザー自身のブラウザ」のみ。

5. Astro移植でハマった「動的DOM」と「スコープ付きCSS」

Astroのコンポーネント内にUIを移植した際、JavaScriptの innerHTML で診断結果カード(.result-card)を動的に生成したところ、スタイル(ネオンカラーの発光バッジやテキスト色)が完全に無視されて素の黒白テキストになる という問題が発生しました。

原因

Astroの <style> タグは、ビルド時にテンプレート内に存在する要素に対して data-astro-cid-xxxxx のような一意の属性を付与し、CSSセレクタをスコープ化します。

/* ビルド後のCSSイメージ */
.badge-pass[data-astro-cid-12345] { color: #00ff88; }

しかし、実行時に JavaScript (innerHTML や document.createElement) で後から作成されたDOM要素には、この data-astro-cid-xxxxx 属性が付与されません。結果としてCSSセレクタがマッチしなくなってしまいます。

解決策

この問題には2つの解決策があります。

  1. JavaScript側でインラインスタイルを展開する 要素生成時に style 属性として直接適用することで、スコープ属性の有無に依存せず確実に描画させます。
card.innerHTML = `
  <span style="
    color: ${mainColor};
    background: ${bgColor};
    border: 1px solid ${mainColor};
    padding: 2px 8px;
    font-size: 10px;
    font-weight: bold;
    text-shadow: 0 0 8px ${mainColor}66;
  ">${data.status}</span>
`;
  1. <style is:global> または :global() を使用する Astroのスコープ化対象から外し、グローバルCSSとして定義します。
<style is:global>
  .badge-pass {
    background: rgba(0, 255, 136, 0.15);
    color: #00ff88;
    border: 1px solid #00ff88;
  }
</style>

6. まとめ

本ツールの開発を通して、単にWebツールを作るだけでなく「ブラウザのセキュリティモデル」「Astroのビルド時CSS変換」「静的サイトにおけるクライアントサイド処理の割り切り」について多くの知見が得られました。

  • CORS診断はプロキシを挟まずブラウザから直接投げるのが最も正確
  • コンソールに出る赤文字CORSエラーはJavaScript側でキャッチできていれば問題なし
  • バックエンドを持たないツールなら、クライアントサイドの軽量認証でも十分に機能する
  • Astroで innerHTML を使う動的DOM描画は、スコープ付きCSS(data-astro-cid)の対象外になる点に注意する

API開発時の動作確認や、CORSエラーの原因切り分けに役立てば幸いです。