[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レイアウト |
スクリーンショット
起動画面
スキャン中
リサルト画面
動画(GIF)
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つあります。
- IP / Origin の相違: 対象APIが特定のOriginやIPゾーンのみ許可している場合、チェッカーのサーバーIPからのリクエストは拒否されてしまいます。
- 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つの解決策があります。
- 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>
`;
<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エラーの原因切り分けに役立てば幸いです。