金利0無利息キャッシング – キャッシングできます

 | 

2010-11-22

twitterのXSSとJSON in ECMAScriptと外部JSONを安全に取り扱うためのアプローチ

23:36 | twitterのXSSとJSON in ECMAScriptと外部JSONを安全に取り扱うためのアプローチ - 金利0無利息キャッシング – キャッシングできます を含むブックマーク はてなブックマーク - twitterのXSSとJSON in ECMAScriptと外部JSONを安全に取り扱うためのアプローチ - 金利0無利息キャッシング – キャッシングできます

twitterXSSがあったので報告した。見つけた/報告した日は16日で、直った日は18日。

  • twitterの人から直ったよと報告を受けていないのだが、俺の知る限り直っていて新規の攻撃は出来なさそうなので安全だろうと判断する。
  • twitter.com以外のサイトにも影響があった可能性があるのだが、それが告知されていないので勝手に告知する。
  • 大きな騒ぎにならなかったのでtwitterはオフィシャルに何か書いたりはしないかもしれない。

概要

  • これはtwitterAPI(twitter.com上のブラウザ用の内部用のものと、OAuthで認証するパブリックなAPI両方)の出力するJSONの生成処理にバグがあり、任意のタグを含めることが出来た問題である。
  • もしあなたがtwitter関連サービスを作っていて、JSONAPIを使っているならあなたのサイトにもXSSがあった可能性がある。
  • このバグが存在していた間に出力されたJSONの発言部分(や、プロフィール文など、タグが使えないと思われている箇所)には、任意のタグが含まれている可能性がある。
  • もしJSON APIを定期的に取得し、それをデータベースに格納しているようなサイトで、発言(や、タグが入らないと思われている箇所)をエスケープせずにHTMLとしてそのまま出力しているのであれば、あなたのサイト上で今でもXSSが可能になっている可能性がある。
    • Twitter側でこの問題が修正されても、あなたが既に受け取ったJSONにはタグが含まれているままなので、あなたのサイトを修正しなければXSSは直らない。

(大きな騒ぎにならなかったので)具体的にどこのサービスで問題が起きたという話は把握してない。しかし少なくとも、バグが存在していた期間中にtwitter widgetsを貼り付けられるサイト上でXSSが可能だったのは把握していて、具体的に危険な挙動になりそうなのは(本来自由にscriptが書けずブログ表示ドメインと管理ドメインが同一である) はてなやgooブログだ。簡単に言えばtwitter.comと同様に「twitterの出力するJSONを信用している」と、twitterのバグに引きづられて、他のサイトにも脆弱性が生まれることになるので、そういう設計は避けなければならない。

長くなりそうなので続きはあとで書く → 書いた

twitterXSS

  • \u2028 \u2029 が JSON内で \ に置換されていた。数値参照使って任意のタグを出力できた。
  • \u2028u003c が \u003c に置換されてしまうので、JSON.parseの結果、「<」になる。
  • で、この記事執筆時点でどうなっているかというと\u2028,\u2029 が \u2070に置換されるようになっている。

発見の経緯

  • 何人からか、twitter関連のサービスで自分のアカウントを表示する際にエラーが出ると報告を受ける
  • 出力されるJSONがinvalidになっているようだと報告を受ける(fujiwaraさんから)
  • どうやら \u2028, \u2029 が \ に置換されているようだ。
  • なにやら悪用できそうな気がしたので(エラーが起こせるのとタグが作れるのだと大分違うので)試して、まずかったのですぐに消した。
  • twitterのセキュリティ担当者にメールした → 3時間後に返信が来た
  • 向こうは夜だったので、twitterapiのIRCで誰か起きてる人が居ないか呼びかけてみたが反応なし。

JSON in ECMAScript

この記事は若干古いので2010年現在のステータスを調べるとする。こういうコードで検証できる。evalだとFirefoxで異常に遅くなったのでFunctionにした。

for(i=0;i<65536;i++) {try { new Function("a", 'var s="' +String.fromCharCode(i)+ '"' ) }catch(e){ console.log(i) } } 

結果はこうなった

Firefox3.610,13,34,92,8232,8233
Chrome810,13,34,92,8232,8233
Safari510,13,34,92,8232,8233
Opera10.6310,13,34,8232,8233
IE9(standard)0,10,13,34,92,8232,8233
IE9(quirks)0,10,13,34,92
  • 10,13,34,92,8232,8233はそれぞれ、 \n, \r, ダブルクオートの中の", \, \u2028, \u2029
  • Operaだけ evalやnew Functionで "\" が入っててもエラーにならない。ソース中に書けばエラーだが。
  • IE9はnulもダメ。互換モードだと\u2028,\u2029通る。

という状況だった。

出力されるJSONJSON.parse以外の手段でパースする場合、つまり

  • jsファイルに埋め込んだり、evalを使って評価する場合

には、JavaScriptのソースとして評価されるので、ECMAScriptで文字列リテラル内に使えない文字 + 処理系依存で幾つかの文字が使えなくなるという状況になるわけだ。\u0020以下の制御文字はエスケープせよとJSONの仕様に含まれているので、古いバージョンのブラウザを除けば問題を起こすのは \u2028, \u2029 である。

で、(やや憶測も含むが)twitterは何らかの理由で出力されたJSON内に含まれる \u2028 \u2029 を置換している。自分はこのような挙動を示すJSONライブラリを知らないし、おそらく生成されたJSON文字列を正規表現で置換する処理が挟まっている。ややこしいのはU+2028のコードポイントを持つ文字ではなく、JavaScriptの数値参照で書かれた状態の "\u2028" を置換している。JSON内では数値参照になっているので、本来置換する必要がないにもかかわらず置換している。

JSONの中に\u2028, \u2029が含まれていて問題が起こるのは、

  • 経路上で、一度JSONがデコードされて、再度(数値参照が使われずに)JSONにエンコードされて
  • さらにそれをJavaScript内に直接埋め込むか、evalを使って評価するケース
  • あるいは、受信したJSONに「ブラウザ側で実行するためのコード」を含んでいて、それをevalするような処理がある場合

もし行うとしても、それはJSONに変換する前に行うべきだと思う。\u2028 を使わずに、直接埋め込んだ場合でなければJavaScript上でエラーは発生しないので、通常置換する必要はない。で、もしユーザー入力にこれらの文字が含まれていて特に対策されていなかったとしても「変な文字いれたらJavaScriptがエラーになる」というだけで、通常脆弱性にはなることはない。

何故こうなったのかの考察

間抜けだと思うが、多分こういうことだと思う。単なる推測なので別の要因があるのかも知れない。これは自分の知る限りtwitter独自の問題で、もし特定のライブラリやフレームワークやミドルウェアで、このような挙動を示すものがあるなら教えてほしい。

json = "\\u2028 \\\\u2028 \\\\\\u2028" # => \u2028 \\u2028 \\\u2028
puts json

# 多分こうなっていた
NG_UNICODE_CHARS = [2028, 2029]
re = /\\u(#{ NG_UNICODE_CHARS.join("|") })/ # => /\\u(2028|2029)/
puts json.gsub(re, "")

# 本来想定しているコード
# \uの前に奇数個 \ が付いていたら何もしない。
re2 = /(\\*)(\\u(#{ NG_UNICODE_CHARS.join("|") }))/
puts json.gsub(re2) {|s|
  $1.length % 2 == 1 ? s : $1
}

間違ったコードの出力結果は

 "\u2028"   → ""
 "\\u2028"  → "\"
 "\\\u2028" → "\\"

こうなる。U+2028のコードポイントを持つ文字は空になり、\u2028はJSONにencodeされる際に \ → \\ となるので \\u2028 になる。それが \u2028 を置換する処理で \ のみ残ってしまう。

影響範囲について

twitterの過去のXSSの多くはtwitter.com上での文字列処理(エスケープ漏れ、自動リンクなど)に起因していたが、今回報告したXSSJSONの出力結果にタグが含まれるという問題なので、JSONを利用していてtweetにタグが含まれない前提で設計している全てのサイトに影響が及ぶことになる。

  • twitter widgetsのJavaScriptコードは、JSONHTMLエスケープ済みであることを信用しているため、twitter widgetsの貼付けを許可しているサイトでもXSSが可能になる
  • 実際にはてなダイアリーに貼りつけたときのスクリーンショット http://gyazo.com/7f517cf3f20a44e585042eb9b37b8b1f.png
    • twitterのwidgetsは、IFRAME内ではなく貼りつけたドメイン上で実行されている(alertの表示にドメインが出てるので確認できる)
    • なのでscriptが混入するとそのドメインの権限で任意の処理を実行したりcookieを盗んだりできる。
  • twitterXSSは過去に何度か「祭り」状態になったことがあるのだが、今回のXSSは悪用されると、twitter.com以外にも影響が及ぶことになる。

API利用者側の対策

TwitterAPIが出力するJSON

  • < や > が &lt; &gt; にあらかじめ置換されている。
    • 例外: sourceプロパティ、使っているクライアント名のリンクがnofollow属性のついたaタグが直接書かれている。
  • これは 受信したJSONを、ほぼそのまま使う場合にはとても楽で、クライアント側でHTMLエスケープする必要がない(二重エスケープになるので)
  • また、いい加減な正規表現で @xxxxx や #xxxxx などのreplyやハッシュタグを置換したり、httpで始まるリンクを置換しても、おおよそ問題は起きないだろう(これは数値参照や実体参照で記述された元の文字が何であるかを考慮しなくても、せいぜいリンク先がおかしくなるぐらいで脆弱性にはならないだろうという話で、本当にいい加減だったらダメです see http://developer.cybozu.co.jp/kazuho/2010/09/twitter-xss-f73.html)
  • もし、まともな開発者であれば、受信したJSONを一度HTMLアンエスケープ(デコード、数値参照や実体参照で書かれた文字を元に戻す = decode html entities)して、必要に応じて自動リンクやフィルタする処理をして、出力するコンテキストに応じてエンコード(文字コードの変換やHTMLエスケープ=そのまま出力されてマズイ文字をencode entities)して出力する
    • APIに含まれる「タグを生かしたまま」使わなければならないのであれば、許可されているタグや属性以外を除去する処理を入れる(ホワイトリスト方式)
  • これが理解出来ていない開発者で、かつ、二重エスケープになることを嫌って「Twitter側でHTMLエスケープ済みである」ということを信頼している場合にはXSSが発生することになる。

これは、相手サービス(あるいは提携関係にあるサービス)のAPIの出力するレスポンスをどこまで信頼できるのか、という問題でもある。例えば、認証情報を扱わないでセキュリティに対して気を使わなくても良いサイト上でJSONPを使ったマッシュアップを行うようなケースで(HTMLエスケープ済みであるということが仕様に明文化されているなら)一度デコードして再度エンコードする、という処理は無駄であるかもしれない、Twitterはある程度信頼できて、TwitterAPIを修正すれば自動で問題は解決するからだ。しかし、一定以上のセキュリティレベルが求められるサイト(個人情報を扱ったり認証cookieを持っている)であれば、(それがHTMLエスケープ済みであると明文化されていたとしても)相手サービスのAPIの仕様変更や、バグがある可能性に備えて、自分たちの責任で出力する段階で安全になるようにエスケープ(エンコード)してやらねばならないだろう。

あとで書く

トラックバック - http://subtech.g.hatena.ne.jp/mala/20101122
 |