JavaScript開発者のためのセキュアコーディング
JavaScriptはブラウザが動作するあらゆる場所で実行されるため、ソフトウェア開発において最も攻撃を受けやすい領域の一つとなっています。ほとんどの脆弱性は特殊なエクスプロイトから生じるのではなく、日常的なコードの予測可能なパターンから発生します。本ガイドでは、コードがブラウザ上で直接実行される際に最も重要となる、セキュアなJavaScriptコーディングプラクティスについて解説します。
重要なポイント
- DOM-based XSSは最も一般的なJavaScript脆弱性の一つです。
innerHTML、eval()、document.write()などのシンクに信頼できないデータを渡すことは避けてください - ユーザー提供データには
innerHTMLではなくtextContentを使用し、HTMLのレンダリングが本当に必要な場合はDOMPurifyでサニタイズしてください - 動的入力に対して
eval()、Function()、文字列ベースのsetTimeout/setIntervalを使用しないでください - nonceまたはハッシュを使用したContent Security Policy(CSP)を適用し、多層防御のためにTrusted Typesと組み合わせてください
- 認証トークンやシークレットを
localStorageやsessionStorageに保存しないでください。セッション識別子にはHttpOnlyクッキーを優先してください postMessageイベントを処理する際は、必ずevent.originを検証してください- 依存関係ツリーを定期的に監査し、CDNから読み込まれるスクリプトにはSubresource Integrityを使用してください
DOM-Based XSS:最も一般的なJavaScript脆弱性の一つ
JavaScriptにおけるXSS防止は、実際にどこで発生するかを理解することから始まります。DOM-based XSSは、コードが攻撃者によって制御可能なソース(location.hash、document.referrer、URLSearchParamsなど)から読み取り、それを安全でない方法でページに書き込む際に発生します。
基本ルール:信頼できないデータを、HTMLとして解釈したりコードを実行したりするシンクに渡さないこと。
動的データで避けるべき安全でないシンク:
// ❌ これらはすべて注入されたスクリプトを実行する可能性があります
element.innerHTML = userInput
element.outerHTML = userInput
document.write(userInput)
eval(userInput)
setTimeout(userInput, 0) // 文字列形式のみ
new Function(userInput)()
安全な代替手段:
// ✅ コンテンツをテキストとして扱い、マークアップとしては扱いません
element.textContent = userInput
element.innerText = userInput
リッチテキストエディタなど、HTMLのレンダリングが本当に必要な場合は、まずDOMPurifyでサニタイズしてください:
// ✅ 安全なリッチテキストレンダリング
element.innerHTML = DOMPurify.sanitize(userInput)
同じロジックがsetAttributeにも適用されます。href、src、イベントハンドラ(onclick、onload)などの動的属性はJavaScriptを実行する可能性があります。ユーザー提供の値を扱う際は、静的で実行不可能な属性に限定してください。
動的コード実行は常に安全ではない
eval()、Function()、文字列ベースのsetTimeout/setIntervalは単なるバッドプラクティスではなく、直接的なコードインジェクションベクターです。信頼できない入力でこれらを安全に使用する方法はありません。
データを解析する場合はJSON.parse()を使用してください。動的な動作が必要な場合は、実行時のコード生成ではなく、データ構造と明示的なロジックを使用してください。
防御層としてのContent Security Policy
Content Security Policy(CSP)は、どのスクリプトが実行でき、どこから読み込めるかを制限します。'unsafe-inline'ではなく、nonceまたはハッシュを使用した厳格なCSPは、万が一XSSが発生した場合の影響範囲を大幅に削減します。
対応ブラウザでは、CSPとTrusted Typesを組み合わせて、APIレベルで安全なDOM書き込みを強制してください。Trusted Typesのブラウザサポートは拡大を続けており、webstatus.devで追跡できます。
Discover how at OpenReplay.com.
セキュアなブラウザAPI:クッキーとクライアントサイドストレージ
セッション識別子を保持するクッキーには、常にHttpOnly(JavaScriptアクセスをブロック)、Secure(HTTPS のみ)、SameSite=StrictまたはLax(CSRF緩和に役立つ)を設定する必要があります。これらをサーバーサイドで設定する方が、JavaScriptから設定するよりも信頼性が高くなります。
**localStorageとsessionStorage**は、ページ上のあらゆるスクリプトからアクセス可能です。認証トークン、セッションシークレット、機密性の高いユーザーデータの保存は避けてください。XSS脆弱性があると、ストレージ内のすべてが即座に露出します。
postMessageによるクロスオリジンメッセージング
postMessageは便利ですが、誤用しやすいAPIです。受信メッセージのデータに対して処理を行う前に、必ずoriginを検証してください:
window.addEventListener('message', (event) => {
// ✅ 処理前に必ずoriginを検証
if (event.origin !== 'https://trusted-origin.com') return
handleMessage(event.data)
})
メッセージを送信する際、固定の送信先がない場合を除き、targetOriginとして'*'を使用することは避けてください。受信側では、メッセージが信頼できるサイトから送信されたことを確認するために、必ずevent.originを検証してください。安全な使用方法の詳細については、postMessageドキュメントを参照してください。
JavaScriptサプライチェーンセキュリティ
JavaScriptサプライチェーンセキュリティは、ますます重要な懸念事項となっています。単一の侵害された、または悪意のあるパッケージが、数千のアプリケーションに影響を与える可能性があります。実践的な対策:
npm auditを実行するか、Snykを使用して依存関係の既知の脆弱性を検出してください- ロックファイル(
package-lock.jsonまたはyarn.lock)をコミットし、予期しない変更を警告サインとして扱ってください - CDNから読み込まれるスクリプトにはSubresource Integrity(SRI)ハッシュを使用してください
- 新しいパッケージを追加する前に監査してください。ダウンロード数、メンテナンス活動、パッケージ名がタイポスクワットである可能性を確認してください
クイックリファレンス:安全なパターンと安全でないパターン
| 安全でない | 安全な代替手段 |
|---|---|
innerHTML = userInput | textContent = userInput |
eval(str) | JSON.parse(str) |
setTimeout(str, n) | setTimeout(fn, n) |
localStorage内のトークン | HttpOnlyクッキー |
originチェックなしのmessage | まずevent.originを検証 |
まとめ
ほとんどのJavaScript脆弱性は同じパターンに従います:信頼できないデータが検証なしで強力なAPIに到達すること。「この値はどこから来たのか、このAPIはそれで何ができるのか?」と問う習慣を身につけてください。この問いを一貫して適用することで、リリース前に大半の問題を発見できます。
よくある質問
常にそうとは限りませんが、ユーザー入力や外部ソースから発生したデータを渡す場合は安全ではありません。動的HTMLをレンダリングする必要がある場合は、まずDOMPurifyのようなライブラリでサニタイズしてください。プレーンテキストコンテンツの場合は、マークアップを解釈したりスクリプトを実行したりしないtextContentを使用してください。
localStorageは、ページ上で実行されているあらゆるJavaScriptからアクセス可能です。攻撃者がXSS脆弱性を悪用した場合、トークンを含むストレージ内のすべてを読み取ることができます。HttpOnlyクッキーは、JavaScriptから全くアクセスできないため、より安全な選択肢であり、クライアントサイド攻撃による被害を制限します。
Reflected XSSは、悪意のあるペイロードがサーバーに送信され、レスポンスでエコーバックされることを含みます。DOM-based XSSはサーバーに到達しません。代わりに、クライアントサイドのJavaScriptがURLフラグメントなどの攻撃者が制御可能なソースから読み取り、それを安全でない方法でページに書き込みます。両方とも危険ですが、DOM-based XSSはサーバーサイドで検出するのが困難です。
CSPは、どのスクリプトソースの実行が許可されるかをブラウザに指示します。nonceまたはハッシュを使用した厳格なポリシーは、インラインスクリプトと未承認の外部スクリプトの実行をブロックします。攻撃者が悪意のあるマークアップをページに注入したとしても、ポリシーに一致しないため、ブラウザはその実行を拒否します。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.