1. XSS の攻撃手段#
XSS(クロスサイトスクリプティング、クロスドメインスクリプト攻撃)攻撃は最も一般的な Web 攻撃であり、コードインジェクション攻撃の一種です。攻撃者はターゲットサイトに悪意のあるスクリプトを注入し、それをユーザーのブラウザで実行させます。これらの悪意のあるスクリプトを利用して、攻撃者はユーザーの Cookie や SessionID などの敏感な情報を取得し、データの安全性を脅かします。その重点は『クロスドメイン』と『クライアントサイド実行』です。
XSS の本質:
- 悪意のあるコードがフィルタリングされず、サイトの正常なコードと混在している;
- ブラウザはどのスクリプトが信頼できるかを識別できず、悪意のあるスクリプトが実行される。
XSS 攻撃には一般的に以下のいくつかの種類があります:
- 反射型 XSS(Reflected XSS)
- ストレージ型 XSS(Stored XSS)
- DOM XSS
- JSONP XSS
タイプ | ストレージ | 挿入ポイント |
---|---|---|
反射型 XSS | URL | HTML |
ストレージ型 XSS | バックエンドデータベース | HTML |
DOM XSS | バックエンドデータベース / フロントエンドストレージ / URL | フロントエンド JavaScript |
JSONP XSS | バックエンドデータベース / フロントエンドストレージ / URL | フロントエンド JavaScript |
1.1 反射型 XSS#
反射型の XSS 攻撃は、主にサーバーがクライアントからの不安全な入力を受け取ることによって、クライアントで実行がトリガーされることで Web 攻撃を引き起こします。
具体的には、反射型 XSS はユーザーが入力したデータを単純にブラウザに「反射」するだけで、この攻撃方法は攻撃者がユーザーに悪意のあるリンクをクリックさせたり、フォームを送信させたり、悪意のあるサイトにアクセスさせたりする必要があります。この攻撃は非持続型です。
例えば:あるショッピングサイトで商品を検索すると、検索結果に検索キーワードが表示されます。検索キーワードに<script>alert('handsome boy')</script>
を入力して検索をクリックします。ページはキーワードをフィルタリングせず、このコードが直接ページ上で実行され、alert が表示されます。
1.2 ストレージ型 XSS#
ストレージ型の XSS 攻撃は、悪意のあるスクリプトを含むコンテンツをサーバーに保存することによって、他の人がそのコンテンツを見るときに Web 攻撃を引き起こすものです。一般的に提出されるコンテンツは、いくつかのリッチテキストエディタを使用して編集されており、危険なコードを挿入するのが容易です。
比較的一般的なシナリオは、攻撃者がコミュニティやフォーラムに悪意のある JavaScript コードを含む記事やコメントを書き、その記事やコメントが公開された後、その記事やコメントにアクセスするすべてのユーザーがブラウザでこの悪意のある JavaScript コードを実行することです。これは持続型の攻撃です。
1.3 DOM XSS#
DOM ベースの XSS 攻撃は、悪意のあるスクリプトによってページの DOM 構造を変更することを指し、純粋にクライアントサイドで発生する攻撃です。
DOM 型 XSS は前の二つの XSS との違い:DOM 型 XSS 攻撃では、悪意のあるコードの取得と実行がブラウザ側で行われ、フロントエンド JavaScript 自身のセキュリティホールに属し、他の二つの XSS はサーバーサイドのセキュリティホールに属します。例を挙げると:
<input type="text" id="input">
<button id="btn">Submit</button>
<div id="div"></div>
<script>
const input = document.getElementById('input');
const btn = document.getElementById('btn');
const div = document.getElementById('div');
let val;
input.addEventListener('change', (e) => {
val = e.target.value;
}, false);
btn.addEventListener('click', () => {
div.innerHTML = `<a href=${val}>testLink</a>`
}, false);
</script>
Submit ボタンをクリックすると、現在のページにユーザーの入力内容をリンクとして挿入します。もしユーザーが入力時に次の内容を構築した場合:
" onclick=alert(/xss/)
ユーザーが送信すると、ページのコードは次のようになります:
<a href onlick="alert(/xss/)">testLink</a>
この時、ユーザーが生成されたリンクをクリックすると、対応するスクリプトが実行されます。DOM 型 XSS 攻撃は、実際にはウェブサイトのフロントエンド JavaScript コード自体が不十分であり、信頼できないデータをコードとして実行してしまったことです。 .innerHTML
、.outerHTML
、document.write()
を使用する際は特に注意が必要で、信頼できないデータを HTML としてページに挿入するのではなく、できるだけ .textContent
、.setAttribute()
などを使用するべきです。
DOM のインラインイベントリスナー、例えば location
、onclick
、onerror
、onload
、onmouseover
など、<a>
タグの href
属性、JavaScript の eval()
、setTimeout()
、setInterval()
などは、文字列をコードとして実行することができます。信頼できないデータが文字列に結合されてこれらの API に渡されると、セキュリティ上のリスクが生じやすいので、必ず避けるべきです。
1.4 JSONP XSS#
JSONP の callback パラメータは非常に危険で、XSS を引き起こす可能性のある二つのリスクがあります:
- callback パラメータが意図せず js コードを切断すること、特殊文字のシングルクォート、ダブルクォート、改行文字がリスクを伴います。
- callback パラメータが悪意のあるタグを追加することにより、XSS 脆弱性を引き起こすことです。
ブラウザはクロスドメインアクセスの安全性を確保するために、デフォルトで callback パラメータをバックエンドに送信します。インターフェースがこのパラメータを受け取った後、返される JSON データを callback パラメータで包む必要があります。
具体的な返却形式:
CALLBACK(JSON)
もし ajax リクエストが JSONP リクエストであれば、返される内容はブラウザによって自動的に検出されます。この形式で返されない場合や callback の内容が正しくない場合、このリクエストは失敗と見なされます。
ここには一つのメカニズムがあり、リクエストの callback が返される内容に含まれることが、問題が発生する可能性のある場所です。例を挙げると、もし返されるページが Content-Type: text/html
であれば、callback 注入された html 要素がページに直接挿入される可能性があります。したがって、html ページは必ず callback をサポートしてはいけません。JSONP をサポートするリンクをブラウザに直接アクセスさせると、ブラウザは callback の検証を行わなくなります。
2. XSS の防御方法#
2.1 XSS 防御の根本的な方法#
前述の説明から、XSS 攻撃には二つの主要な要素があります:
- 攻撃者が悪意のあるコードを提出する。
- ブラウザが悪意のあるコードを実行する。
根本的な解決策:入力から出力までフィルタリングとエスケープが必要です。
入力#
入力はクライアントのリクエストパラメータを指し、具体的には:
- ユーザー入力
- URL パラメータ
- POST パラメータ
HTML コードに対するエンコーディング方式は HTMLEncode であり、その役割は文字列を HTMLEntities に変換することです。現在、XSS に対抗するためには、以下の六つの文字をエンティティ化してエスケープする必要があります。
特殊記号 | エンティティコード |
---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
/ | / |
もちろん、上記は最も基本的で必要なものであり、HTMLEncode には他にも多くのものがあります。具体的には、Web 安全系列(四):XSS の防御 | 掘金 に記載されている特殊文字を参照してください。
さらに、リッチテキストの入力には特に注意が必要です:
- まず、入力チェックを行い、ユーザーが完全な HTML コードを入力していることを確認し、結合されたコードではないことを保証します。
htmlParser
を使用して HTML コードのタグ、属性、イベントを解析します。- リッチテキストのイベントは必ず禁止する必要があります。リッチテキストにはイベントは必要ありません。また、危険なタグも禁止する必要があります。例えば:
<iframe>
、<script>
、<base>
、<form>
など。 - ホワイトリストメカニズムを利用して、安全なタグのみを埋め込むことを許可します。例えば:
<a>
、<img>
、div
など。ホワイトリストはタグだけでなく、属性にも適用されます。 - ユーザーの CSS をフィルタリングし、危険なコードがないか確認します。
出力#
入力時にフィルタリングを行ったからといって万事解決ではありません。悪意のある攻撃者は防御メカニズムを層をなして回避し、XSS 攻撃を行う可能性があります。一般的に、HTML ページに出力する必要のあるすべての変数は、すべてエンコーディングまたはエスケープを使用して防御する必要があります。出力がエスケープが必要な部分は、具体的には:
- HTML で出力する場合
- JavaScript で出力する場合
- CSS で出力する場合
- URL で出力する場合
HTML での出力#
HTML の部分は入力のエスケープ方式と同じで、HTMLEncode を使用します。ここでは繰り返しません。
JavaScript での出力#
JavaScript の部分も同様にエンコーディングエスケープが必要です。例えば、JSONP の中で意図せず JSON データを切断したり、ページ内で引用符を操作したりすることで XSS 攻撃を引き起こすことがあります。
let a = "私は変数です"
// 私は変数です = ";alert(1);//
a = "";alert(1);//"
攻撃者はタグを閉じるだけで攻撃を実行できます。現在の防御方法は JavaScriptEncode です。JavaScriptEncode は HTMLEncode のエンコーディング方式とは異なり、特殊文字を \
でエスケープする必要があります。
CSS での出力#
CSS 内や style タグ内での攻撃手法は非常に多様です。具体的には、Web 安全系列(四):XSS の防御 | 掘金 を参照してください。ここでは、長さの問題のため、解決策について簡単に触れます。
CSS の攻撃問題を解決するためには、一方でユーザーが変数を style タグ内に入力することを厳格に制御し、他方で未知の CSS ファイルを参照しないことが重要です。もしユーザーが CSS 変数を変更する必要がある場合は、OWASP ESAPI
の encodeForCSS()
関数を使用することができます。
URLEncode での出力#
URL での出力は直接 URLEncode を使用すればよく、変数の部分をエスケープする必要があります。
2.2 その他の XSS 防御方法#
JSONP XSS の防御方法#
Content-Type: application/json
を厳密に定義します。ブラウザのレンダリングは Content-Type に依存しています。もし返される内容のマークが json であれば、たとえ body の中に html タグがあっても、ブラウザはレンダリングしません。したがって、インターフェースが返す内容が html でない場合は、絶対に html として書かないでください。Content-Type を乱用しないでください。標準プロトコルに従って厳密に行ってください。現在のフレームワークはデフォルトで内容タイプを検出しますので、あまり必要でない限り手動で設定しないでください。なぜなら、Content-Type が何度も転送されるうちに変更される可能性があるからです。- callback に長さ制限を設けます。これは比較的単純で、一般的に関数名は 50 文字以内に制限されます。
- callback 内の文字を検出します。一般的に callback 内には文字と数字が含まれ、他の記号は含まれてはいけません。関数名は
[
,]
,a-zA-Z0123456789_
,$
,.
のみを許可し、一般的な XSS や utf-7 XSS などの攻撃を防ぎます。 - callback および JSON データの出力をフィルタリングします。原理は出力エスケープと同じです。
- その他の「卑劣な」方法:Callback 出力の前に他の文字(例えば:
/**/
、改行)を追加することで、JSON ファイルの読み込みに影響を与えず、他のファイル形式の出力をある程度防ぐことができます。また、Gmail が初期に AJAX を使用して JSON を取得する際に、JSON 出力の前にwhile(1);
のようなコードを追加して JS のリモート呼び出しを防ぐことがありました。
Web セキュリティヘッダーのサポート#
これはブラウザが持つ防御能力であり、一般的には Web セキュリティヘッダーを有効にすることで機能します。具体的には以下のいくつかがあります:
- CSP:W3C のコンテンツセキュリティポリシー、略して CSP は、ページがどのリソースを読み込むことができるかを定義し、XSS の発生を減少させるために使用されます。CSP を構成するには、CSP のポリシー戦略について理解する必要があります。具体的な詳細は CSP とは を参照してください。
- X-Download-Options: noopen:デフォルトで有効で、IE のダウンロードボックスの Open ボタンを無効にし、IE でダウンロードファイルがデフォルトで開かれるのを防ぎます。
- X-Content-Type-Options: nosniff:IE8 の自動 MIME 機能を無効にし、例えば
text/plain
をtext/html
としてレンダリングすることを防ぎます。特に、当サイトのサーバーの内容が必ずしも信頼できない場合に重要です。 - X-XSS-Protection:IE が提供する XSS 検出と防止機能で、デフォルトで有効です。
HTTP-only Cookie#
HttpOnly は最初に Microsoft によって提案され、現在では標準となっています。ブラウザは HttpOnly 属性を持つ Cookie へのページの JavaScript アクセスを禁止します。
攻撃者は悪意のあるスクリプトを注入してユーザーの Cookie 情報を取得することができます。通常、Cookie にはユーザーのログイン資格情報が含まれており、攻撃者が Cookie を取得すると、Cookie ハイジャック攻撃を行うことができます。したがって、厳密に言えば、HttpOnly は XSS 攻撃を防ぐのではなく、XSS 攻撃後の Cookie ハイジャック攻撃を防ぐことができます。
// express を利用して cookie を設定し、httpOnly を有効にする
res.cookie('myCookie', 'test', {
httpOnly: true
})
CAPTCHA メカニズムの追加#
スクリプトがユーザーを偽装して危険な操作を提出するのを防ぎます。
3. XSS の経験まとめ#
全体的な XSS 防止は非常に複雑で面倒です。私たちはすべてのエスケープが必要な場所でデータを適切にエスケープする必要があります。また、余分で誤ったエスケープを防ぎ、正常なユーザー入力が文字化けしないようにする必要があります。技術的手段で完全に XSS を回避することは難しいですが、以下の原則をまとめて脆弱性の発生を減少させることができます:
以下の経験まとめは《【基本功】 フロントエンドセキュリティシリーズの一つ:XSS 攻撃を防ぐ方法 | 美団技術チーム》からの抜粋です。
-
テンプレートエンジンを利用する
テンプレートエンジンに備わっている HTML エスケープ機能を有効にします。例えば、ejs では<%= data %>
を使用することを推奨し、<%- data %>
は避けます;doT.js では{{! data }
を使用し、{{= data }
は避けます;FreeMarker では、エンジンのバージョンが 2.3.24 より高いことを確認し、正しいfreemarker.core.OutputFormat
を選択します。 -
インラインイベントを避ける
onLoad="onload('{{data}}')"
、onClick="go('{{action}}')"
のようなインラインイベントの結合を避けるようにします。JavaScript では.addEventlistener()
を使用してイベントをバインドする方が安全です。 -
HTML の結合を避ける
フロントエンドで HTML を結合する方法は非常に危険です。フレームワークが許可する場合は、createElement、setAttribute などの方法を使用します。また、Vue/React などの成熟したレンダリングフレームワークを使用することも推奨されます。 -
常に警戒を怠らない
挿入位置が DOM 属性、リンクなどの場合は、注意を払い、厳重に防御します。 -
攻撃の難易度を上げ、攻撃の結果を軽減する
CSP、入力長さの設定、インターフェースのセキュリティ対策などの方法を通じて、攻撃の難易度を上げ、攻撃の結果を軽減します。 -
積極的に検出し、発見する
XSS 攻撃文字列や自動スキャンツールを使用して潜在的な XSS 脆弱性を探します。