Web3のフロントエンドは「信頼の最後の砦」だ:CSPでウォレットドレインを物理的に封殺せよ
いいか、よく聞け。Web3界隈で「スマートコントラクトの監査は完璧だ」と胸を張るプロジェクトは山ほどあるが、そのフロントエンドがスカスカで、ユーザーの資産がごっそり抜かれる事例を俺はいくつも見てきた。
コントラクトがどれほど堅牢でも、フロントエンド(DApp)がXSS(クロスサイトスクリプティング)に無防備なら、攻撃者はユーザーのブラウザ上で「偽の署名要求」を捏造する。ウォレットの接続ボタンを押した瞬間、裏で eth_sendTransaction が改ざんされたら終わりだ。
今日は、そんな悪夢を未然に防ぐための「CSP(Content Security Policy)」の実装術を叩き込む。教科書的な説明は省略する。現場で「効く」設定だけを教えるぞ。
—
攻撃者の狙い:なぜDAppのフロントエンドが標的になるのか
攻撃者は、JavaScriptのライブラリ(ethers.js や web3.js)をフックして、provider.request({ method: 'eth_sendTransaction', ... }) の引数を書き換える。
もし君のサイトに悪意あるスクリプトが混入した場合、CSPがないと、攻撃者は外部の悪意あるドメインへデータを送信したり、勝手なスクリプトをロードし放題だ。これを防ぐ唯一にして最強の防御線が Content-Security-Policy ヘッダーによる「ブラウザへの強制命令」だ。
—
実践:最強のCSPポリシーを定義する
Web3アプリにおいて、許可すべきは「自分のドメイン」と「信頼できるRPCノード」、そして「ウォレット接続に必要な特定のスクリプト」だけだ。それ以外はすべて遮断する。
以下は、Nginxで設定すべき、実務レベルで推奨するCSPヘッダーだ。
# Nginxの設定ファイル内(server または location ブロック)
# 安全のために厳格なポリシーを適用する
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' https://trusted.cdn.com;
connect-src 'self' https://mainnet.infura.io https://api.walletconnect.com wss://relay.walletconnect.org;
frame-src 'self' https://verify.walletconnect.com;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
" always;
この設定のポイント:
default-src 'self':デフォルトで外部からの読み込みを全て拒否。connect-src:これがWeb3の生命線だ。API通信やウォレット接続先(InfuraやWalletConnect)のドメインをホワイトリスト化する。これ以外への通信はブラウザが強制ブロックする。object-src 'none':レガシーな攻撃(プラグイン等)を無効化。frame-src:WalletConnectのモーダル等がiframeを使う場合、許可リストを適切に管理する。
—
バックエンドでの動的生成(PHP/Node.jsでの実装例)
Nginxで一括設定できない場合や、nonce(ナンス)を使ってインラインスクリプトを制御したい場合は、アプリケーション側でヘッダーを制御する。
PHPでの実装例:
<?php
// 一意なナンスを生成(スクリプトごとに使い回さないのが鉄則)
$nonce = base64_encode(random_bytes(16));
// CSPヘッダーを送信
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-$nonce'; connect-src 'self' https://mainnet.infura.io;");
// HTML出力時
echo "<script nonce='$nonce'>console.log('これは実行される');</script>";
echo "<script>alert('これはブロックされる');</script>";
?>
—
現場でハマる「落とし穴」とデバッグのコツ
「CSPを導入したらサイトの表示が崩れた」「ウォレットが繋がらなくなった」と泣きついてくる後輩が多い。だが、それはブラウザのデベロッパーツール(コンソール)を見れば一発で解決する。
CSPによってリソースがブロックされた場合、ブラウザは親切にもエラーログを吐く。
> Refused to connect to 'https://malicious-site.com' because it violates the following Content Security Policy directive...
このログが出たら、「本当にそのドメインが必要か?」を自問自答しろ。必要なら connect-src に追加し、不要なら攻撃の兆候だ。
セキュリティチーフからのアドバイス:
1. unsafe-inline は絶対に使わない:どうしても必要な場合以外は排除しろ。XSSの温床だ。
2. report-uri を活用する:ポリシー違反が発生した際に、自分の監視サーバーへログを飛ばす設定をしておけ。インシデントの予兆検知に役立つ。
3. 常にテストモードを挟む:Content-Security-Policy-Report-Only ヘッダーを使えば、ブロックせずに違反だけを通知してくれる。まずはこれで本番環境のログを1週間監視してから、正式に適用しろ。
—
結論:セキュリティは「多層防御」の積み重ね
CSPは魔法ではない。しかし、攻撃者がフロントエンドを掌握した瞬間に「送金先を書き換える」という最後の凶行を物理的に遮断するための、最もコストパフォーマンスの良い防壁だ。
「面倒だから」といって設定を疎かにするな。君が書いたコードの先に、誰かの大切な資産があることを忘れるなよ。次は、React/Next.js環境におけるより詳細な実装と、WalletConnectのセッションハイジャック対策について掘り下げる。準備しておけ。
コメント