XSSがある場合のCSRF対策としてクリックジャッキングを使う(没)案
最近話題になっているクロスサイトリクエストフォージェリの話を。
CSRF対策はXSSがある場合には効果的ではないと言われます。javascriptが実行できればhtml中のtokenもとれますし、リクエストも自由に送信できるので対策を回避できるためですね。
XSSがあっても万が一防げるようにするため、クリティカルな箇所ではパスワードなどユーザしか知らない情報を使って確認しているサイトが多いでしょうか。しかしユーザに何度もパスワードを入力を促すと、XSSによって入力フォームが作られたときに疑われずに入力されてしまうのではないかという懸念もあります。*1
javascriptから直接触ることができない領域を使えばXSSがあったとしてもなんとかなるはずです。思いつくのはhttponlyがついたクッキーの値や別オリジンでしょうか。httponlyがついたクッキーは基本的にリクエスト時に送信されているのでCSRF対策にはなりません。他にもFlashとかでなにかあるかもしれませんが、まあ置いておきます。
ここでは別オリジンを使った方法を考えます。最も単純なのはユーザがボタンを押したときに同時に別オリジンのボタンを押させることです。そうクリックジャッキングです。
クリックジャッキングについて
詳細はこのあたりで。
大体の場合はレスポンスヘッダにX-Frame-Options
を使ってiframeに読み込まれるのを防ぎます。
CSRF対策案
CSRFを防ぎたいhttps://example.com
と、別オリジンとなるhttps://prevent-csrf.example.com
を用意します。htmlの雰囲気としてはこんな形で。
// https://example.com の画面 ... <form> <input type="token" value="何かしらのcsrf_token" /> <button type="submit">XSSがあってもCSRFを防ぎたい処理のボタン</button> </form> <iframe src="https://prevent-csrf.example.com/prevent" style="...."> <form action ="/prevent"> <input type="referrer" value="読み込みサイトのURL" /> <input type="token" value="何かしらのcsrf_token_2" /> <button type="submit">元のドメインのボタンと同じ位置に表示されるボタン</button> </form> </iframe>
ボタンが押されたときにそれぞれのドメインに向けてほぼ同じタイミングでリクエストが飛ぶようにし、それをサーバ側で検証することでユーザ操作によるものか、XSSか何かを使って操作されたものかどうかを判断します。
ボタンが押されたときに同時にリクエストされることを想定していますが、ちゃんと動くかどうかは微妙なのでhttps://example.com
側はjavascriptで制御してタイミングを遅らせるかSingle Page Applicationのような画面遷移にしたほうが良いでしょうか。
問題点
アクセスビリティ
マウスイベントはクリックジャッキングによってiframeの埋め込み先に貫通しますが、tabキーでのフォーカスは一緒に遷移はしませんし、エンターキーでのフォームの送信もされません。スクリーンリーダーとの組み合わせも悪いでしょう。ユーザに対して、"マウスでクリックして"と説明をつけることである程度回避できるかもしれませんが、使いづらそうです。
CSRF対策
例のhttps://prevent-csrf.example.com
側にもCSRFとXSS対策も必要です。CSRFが行われると結局元のドメインにもCSRFができてしまうためです。この用途にしか使わないドメインとしておいて、さらにCSPなど入れてガチガチにするとこんな感じでしょうか。
- 用途を限定して他の目的に使わない
- httpsのみにし、さらにHSTSをかける
- CSPでXSSをブロックする
- X-Frame-Optionsで
ALLOW-FROM
を使ってiframeとして読み込み元を限定する*2
XSS対策の一部として全くjavascriptを使わない実装にするか、javascirptを使ってカスタムヘッダなどをつけて送信することでCSRF対策にするかは迷うところですね。
https://prevent-csrf.example.com
を攻撃者が自分で開く(or サーバから自動で叩く)こともありえるので、*.example.com
に対してhttponlyとsecureフラグをつけたトークンを更に別に発行して、サーバ側で同じユーザかどうかを判断する必要もあります。サブドメインであればこのあたりはやりやすいですが、そうでない場合はOAuthとかリダイレクトでトークンを渡してやる必要があって、更にその仕組の安全性が…となっていくのでひたすら面倒ですね。
CSRF対策としての機能を目指しているのに、更にCSRF対策をしているのは何と言うか、何でしょうね。
クリックジャッキング対策(追記22:12)
元のオリジンにクリックジャッキング対策が必要なのはもちろんですが、XSSによってjavascriptが実行された時ユーザのマウスに追随する形で、https://prevent-csrf.example.com
側のiframeを移動させることも可能です。そのため多少面倒ですが、XSSとクリックジャッキングの組み合わせによってCSRFができてしまう可能性があります。window.screenX
を使って、iframeが読み込まれている位置を特定できるかもしれませんが確認できていません。
まとめ
*1:この辺どっちがいいか資料などがあれば誰か教えてください。
*2:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options#Browser_compatibility によるとChromeではALLOW-FROMが使えないようです
ChatWorkでのStored XSS
はじめに
この脆弱性は BugBounty.jp を通して報告しました。利用規約(第10条の5)によれば法人会員の許可があれば公開可能のようだったので、脆弱性を報告した際に公開可能かも問い合わせ、承諾を得ています。現在はこの脆弱性は修正されています。
詳細
チャットワークではURLらしきものが入力されると、自動的にリンクになる機能があります。URLのドメインなどによって挙動がさらに変わるのですが、外部サイトのプレビュー機能の部分でStored XSSとなる部分がありました。
修正前のソースのChromeの開発者ツールで整形したものから該当箇所を抜粋してます。
... if ("www.youtube.com" === t.hostname) { if (!(o = t.href.match(/\/watch\?v=([^\/&]+)/))) return; n.type = "youtube", n.content_id = o.pop() } ... return n.type ? ' <a\n class="_previewLink timelineLinkAppend"\n data-url="' + c.urlencode(e) + '"\n data-type="' + n.type + '"\n data-user-id="' + n.user_id + '"\n data-content-id="' + n.content_id + '">' + p.Language.getLang("%%%preview%%%") + "</a>" : "" }
YoutubeのURLであれば、URL中のv=
より後ろの文字列がcontent_id
に入ります。*1
content_id
を使っている部分ではそのままHTMLの組み立てに使われています。
data-content-id="' + n.content_id + '">'
URLとして判別される文字種は限られているのですが、"
は入力可能です。そのため
https://www.youtube.com/watch?v=#"onmouseover=alert(location.host)*"
と言った文字列が入力されるとXSSとなります。
発火しないパターン
ChatWork中では空白文字はURLとしては認識されないため、たとえば以下の方法では imgの後ろに空白が入るのでそこで途切れます。
https://www.youtube.com/watch?v=#"><img onerror=alert(locaton.host)>
owaspの資料によると空白の代わりに/
も使えるようですが、それもURLからパラメータを取得する正規表現の段階で弾かれます。
ですので、
"><script>...</script>
も記述できません。、/
が書けないため</script>
を書くことができず、別の所にある</script>
までをscriptタグの中身として認識されるのでシンタックスエラーとなります。"
や'
、`で文字列にしたりしてしましたが文字列が明示的閉じられないとやはりシンタックスエラーとなりました。*2
タグを追加することができますが閉じれず、追加するタグについても属性はかけません。しかし、a
タグの属性に追加することは可能です。
イベントハンドラを使った攻撃
さて仕方がないのでイベントハンドラでなんとかします。a
タグであるため使えるイベントハンドラは限られていてonload
などは動きません。おそらくもっと使いやすいものはonmouseover
です。
そのまま使うとユーザがマウスオーバするのを誘導しないといけないので攻撃としてはちょっと難しいですね。a
タグの属性は自由に追加できたのでスタイル属性を追加してcssで装飾してやります。
本当はモーダルのように画面いっぱいにして部屋を開いた段階で回避不可能なようにしたかったのですが、CSSにあまり詳しくないため画面半分ぐらいを埋めるのが限界でした。
自由に攻撃する
イベントハンドラで攻撃が可能になりましたが、使用できる文字種に制限があるとそのまま実行できるjavascriptにも制限がかかってしまいます。 今回は()
が使えるのでevalとbase64を使って回避できました。
// btoa('alert("xss")') // > "YWxlcnQoInhzcyIp" https://www.youtube.com/watch?v=#"onmouseover=eval(atob(`YWxlcnQoInhzcyIp`))*" // -> alert("xss") が実行される。
対策
修正後に確認したところ、encodeURI
でエスケープがされていました。
こういった正規表現の考慮漏れなどに対して抜本的な対策としては、生のHTMLを組み立てるのではなく、仮想DOM系のライブラリを使うのが良いです。ですが、すでに動いているシステムを置き換えていくのはまあ難しいのはあります。(ソースを見るとReactは入っているようですが)CSPを入れるという手もありますが、事前に確認する点が多そうです。
影響
Stored XSSであり、複数ユーザが閲覧できるそれぞれの部屋に書き込みもできるため他のユーザにも影響が及びます。また、チャットワークは複数組織にまたがったチャット部屋が存在する(確かAPI関連の部屋があったはず)ため、ワームのように影響が広がる可能性がありました。*3 数年前に起きたtwitterでのワームと類似しています。
また登録しているメールアドレスを変更する際にパスワードの入力がないため、パスワードリセットの機能との組み合わせでおそらくアカウントを乗っ取れます。*4
またチャットワークは過去の自分の発言も編集できるので、過去に遡って発言を改ざんすることが可能です。
デスクトップ版のアプリではプレビュー機能がないため影響を受けないようでした。おそらくElectron製なので外部リソースを読み込まないようにしているのではないでしょうか。同様にAndroid版もプレビュー機能がないので影響を受けていません(iPhoneは持ってないのでわかりません)。
影響度が高い攻撃方法として考えられるのは以下の組み合わせです。
- ワームとして広がるようにつくる
- 攻撃によって実行している最中にパスワードの確認入力を求めるダイアログを出す
- auto completeで自動入力された場合は送信する
- ユーザが入力した場合はそれも送信する
- 裏で行う処理として
- メールアドレスを変更し、パスワードリセット機能からパスワードを変更して乗っ取る
- チャットの内容を変更
- チャットの内容を流出
- 部屋のメンバーを変更する
- パスワードを取得できた場合
- 外のサービスでの使いまわしされてないかを探る
service workerが使えるともっと幅が広がるのですが、ファイルアップロード先が別ドメインのため難しいでしょう。
ランサムウェアのような振る舞いの可能性 (追記23:41)
チャットの内容を変更したり、アカウントの乗っ取りが出来るということはランサムウェアのようにチャットの内容を対象に身代金を請求される可能性が出てきます。変更前のメールアドレスに対して、ビットコインの振込先を指定するような形になるでしょうか。
webサービスに対してのランサムウェア的行為はDDoS以外では聞いたことはないですが、チャットワークが仕事で使うツールであり、リアルタイムなやり取りを目的としたサービスであること、仮想通貨の流通量が増えてることなどを考えると割と現実味がありそうです。チャットワーク側が対策を打つか復旧させるまでの時間との勝負になるので、早期割引等があると効果的かと。特にファイルは削除されると(UI上では)元に戻せないので、その場合どうなるのかは気になるところです。
ワームが広がったときの対策
twitterとは違い過去の内容が変更できるため、チャットの内容を変更されるのが地味に一番イヤな気はします。ユーザによるものかどうかが判別が難しいので、全部の変更のログを確認して攻撃によるものかどうかを判別するか、発覚時点までのDBに戻すか、諦めるかになるでしょうか。
余談
数カ月ぶりにbugbonty.jpで報告したのですが、送信時にCVSS V3での評価が必要になっていました*5。評価の経験があまりないのでXSSの標準(5.4)をそのままに使いましたが、今回の脆弱性ですと影響範囲が大きいのでこれでいいんだろうかという気持ちにはなりました。
モバイル向けとPC向けの切り替えボタンでDom Based XSS
ほぼタイトル通りですが、これも数年前なのでちょっと古い話です。
数年前、まだスマートフォンが今ほどのシェアでない時代、PC向けの画面表示とモバイル向けのページが混在しているような状況でした*1。そんな状態でモバイルからPC向けのページを見たい人向けにはそれ用の導線があったり、間違えてPC向けのページに来てしまった人向けの導線が用意されていました。
サンプル
モバイルのuser agentでのモバイル向けの表示
モバイルのuser agentでPC表示
ここで問題となっていたのが、"スマートフォン版へ"のリンクの内容です。PC版とモバイル版でほぼ同じURLを使っているような場合、javascriptでボタンが生成されている事があります。
このケースの場合、一度PC版向けの表示をさせなければ行けないということで結構面倒だったのですが手順を踏むとXSSが可能でした。*2
当時報告先がわからなかったので、運営元のlivedoor ブログの(確か)問い合わせフォーム経由で報告したのですが、数日ほどで対応されました。
htmlを動的に組み立てるのは問題が起きやすいので、何かしらのライブラリでエスケープするか、aタグのDOMを生成して操作するのが無難ですね。*3
(記事公開当初、サンプル画像がPC向けとモバイル向けで逆になっていたのを修正しました。)
webメールサービスのリンク確認画面にあった反射型XSS
数年前、EUC-JPなwebメールサービスでたまたま反射型のXSSを見つけたのでIPAに届けていました。
当時eメールからのリンクをクリックしたときの動作を調べていたのですが、その際に変な挙動に突き当たりました。適当なリンクを自分自身に送信した後、画面内に表示されたリンクをクリックすると外部リンクであることの確認画面に飛ばされます。そこまではいいのですが、なぜかリンクが途中で切れていました。
webメールの安全性は結構重要でして、webサービスではパスワードリセットする際にはメールで確認しているケースが多いため*1、ここに脆弱性があると他のサービスにも影響を受ける可能性が高まります。というわけで、よくよく調べてみると、確認画面のクエリパラメータに載っているurl=
の文字の一部が画面側には出ていません。
このあたり記憶が曖昧なのですが xss に繋がりそうな文字種があると画面に表示する際には消されていたり、WAFでブロックされたような表示になっていた覚えがあります。詳細については念のため省略しますが、パラメータに細工をすることで対策された部分を乗り越えることができました。
chromeでは反射型XSSはフィルタリングによってブロックされるので単純には実行できませんでしたが、デフォルトでは組み込まれていないFireFoxでは実行できました。
ユーザが悪意のあるリンクをクリックするのを防ぐためのセキュリティ対策と思われますが、実装に脆弱性があるケースでした。*2
IPAを通じて届け出たのですが思いの外対応が早く、最初のメール送信から受理と通知までに3日ほど、修正までに一週間ほどでした。*3どこまで影響があるか調べるよりも先に届け出るのを優先したため、実際にどこまで影響があるのかを確認していなかったのですが、おそらくメールの読み取りなどはできたのではないでしょうか。
この手の反射型のXSSに対する対策としては、基本的にフレームワークに使われているエスケープ関数を使えば大抵の場合問題ありません。使っていない場合はフレームワークを入れるかライブラリを探すか、徳丸本を読みましょう。*4