【React+Rails】XSS攻撃して、CSPで対策してみる
はじめに
LocalStorage や Cookie で認証情報を保持すると、XSS の危険性があります。
本記事では、実際に XSS 攻撃をして、CSP で対策をすることでセキュリティの知識を深めることを目的とします。
XSS と CSP
XSS とは
XSS(Cross-Site Scripting)脆弱性とは、悪意のあるユーザーが挿入するスクリプトが他のユーザーのブラウザ上で実行されてしまうセキュリティ上の問題です。
例えば、ユーザーが投稿したコンテンツをエスケープ処理せずに表示している場合、攻撃者は Local Storage からアクセストークンを取得したり、Cookie を利用して認証が必要なページの情報を抜き出したりできます。
CSP とは
CSP(Content Security Policy)とは、読み込むことのできるリソースの種類や取得先をあらかじめ Web アプリケーション側で宣言しておき、Web ブラウザがその宣言に違反する挙動を検知する仕組みです。
HTMLのヘッダー で読み込むことのできるリソースの種類や取得先を宣言することで、攻撃者による意図しないスクリプトの実行を防ぐことができます。
デモアプリケーション作成
Rails と React でタイトルとコンテントを投稿するアプリケーションを作成しました。
"アクセストークンの中身"
という値をローカルストレージに保存し、dangerouslySetInnerHTML
を使用して投稿一覧を表示しています。
useEffect(() => {
// アクセストークンをローカルストレージに保存
localStorage.setItem('accessToken', "アクセストークンの中身")
fetchPosts()
}, [])
return (
...
<h1>投稿一覧</h1>
{posts.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* <div>{post.content}</div> ※補足 */}
</div>
))}
</div>
)
🔗【※補足】React の dangerouslySetInnerHTML
XSS 攻撃する
さっそく、悪意のあるスクリプトを投稿し、閲覧した人のアクセストークンを盗み出します。
title部分に適当な文字列、content部分に下記を記入します。
<img src="invalid-image" onerror="alert(localStorage.getItem('accessToken'))">
これは HTML の<img>要素を使用し、画像の読み込みに失敗した場合に JavaScript の alert() 関数を呼び出し、localStorage から accessToken というキーの値を取得して表示するコードです。
登録ボタンを押すと、Local Storage に保存していたアクセストークンの中身
がダイアログに表示されました!
今回は省きましたが、実際の攻撃では取得したアクセストークンを攻撃者のサーバーに送信することでアクセストークンを盗み出し悪用します。
CSP で XSS 攻撃対策する
次に、CSP を活用して JavaScript の実行を制御し、XSS 攻撃対策をします。
HTMLのヘッダーを動的に操作できるようにする
CSP はHTMLのヘッダーで設定されるため、React ではreact-helmet-async
ライブラリを使用します。
下のように、<Helmet>
内で CSP の設定を行います。
import { Helmet, HelmetProvider } from 'react-helmet-async';
...
<HelmetProvider> {/* Providerコンポーネント内で状態を管理 */}
<App>
<Helmet> {/* Helmetコンポーネント内で動的にHTMLのhead要素を変更する */}
<meta
httpEquiv="Content-Security-Policy" // CSP を設定
content="script-src 'self';
img-src 'self';
connect-src http://localhost:3001"
/>
</Helmet>
</App>
</HelmetProvider>
CSP の設定をする
content=...
の箇所はディレクティブセットといい、"制限するリソースタイプ"と"読み込みが許可されるソースや実行を許可するアクション"のセットを記述します。
今回は3つディレクティブセットを設定しました。
-
script-src 'self'
<script>やinlineスクリプトに対する制限:'self'
からの読み込みを許可 -
img-src 'self'
<img>やCSSのbackground-imageに対する制限:'self'
からの読み込みを許可 -
connect-src http://localhost:3001
JavaScriptの実行などで発行されるHTTPリクエストの宛先URLに対する制限: Railsサーバーへの接続を許可
(※ 'self'
:現在のドメインを指定するためのキーワード)
これでCSP による XSS 攻撃対策が完了しました。
CSP で XSS 攻撃対策できているか確認する
それでは、先程と同じ悪意のあるスクリプトを投稿し、対策ができているのか確認します。
<img src="invalid-image" onerror="alert(localStorage.getItem('accessToken'))">
登録ボタンを押してもダイアログは現れずに、CSP によりインラインイベントが拒否されたという内容のlogが表示されました!
Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self'".
CSP により XSS 攻撃対策できていることが確認できました。
dangerouslySetInnerHTML
【※補足】React の実は React で使用する JSX 構文は、HTML タグをエスケープする仕様になっています。
自分のアプリケーションで明示的に書かれたものではないあらゆるコードは、注入できないことが保証されます。レンダーの前に全てが文字列に変換されます。これは XSS (cross-site-scripting) 攻撃の防止に役立ちます。
なので、ブログアプリなどで意図的にユーザーが HTML タグを実行できるようにするには、 dangerouslySetInnerHTML
を使用します。
しかし、dangerouslySetInnerHTML
は、内部的に JavaScript のinnerHTML
を使用しているため、<script>
の実行はされないようになっています。なので、今回行った XSS 攻撃では mdn の例にあるように<script>
ではなく<img>
を使用しています。
dangerouslySetInnerHTML
を使用していない React アプリに XSS 攻撃する
埋め込まれた値がエスケープされるか確認します。
{/* <div dangerouslySetInnerHTML={{ __html: post.content }} /> */}
<div>{post.content}</div>
上のように<div>{post.content}</div>
に置き換えて同様の悪意のあるスクリプトを投稿します。
imegeのアイコンではなく、エスケープされた文字列が表示されることを確認できました。
参考文献
Discussion