Twitter の BASIC 認証が廃止されて、外部アプリ・サービスは OAuth 認証が必須になったわけですが、この OAuth 認証パラメーターの渡し方でおもしろいことに気付きました。
通常、OAuth 認証は次のようにアカウント認証に必要なパラメーターを HTTP リクエストの Authorization ヘッダで渡します。
GET /1/statuses/home_timeline.json?count=100 HTTP/1.1 Authorization: OAuth oauth_consumer_key="???",oauth_signature_method="HMAC-SHA1",oauth_timestamp="0000000000",oauth_nonce="???",oauth_version="1.0",oauth_token="00000000-????",oauth_signature="????" Host: api.twitter.com Accept-Encoding: gzip, deflate User-Agent: boeboe Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 Connection: keep-alive
これは BASIC 認証と同じ方法で、Authorization ヘッダにあるパラメーターでもってこのリクエストがどのアカウントを使ったものかを特定できるようになっています。ここに OAuth プロトコル的な正しいパラメーターを入れておかないと「アカウントが違います」的なエラーになる、という寸法ですね。
上の例ではセキュリティ的なアレを考慮して「正しいパラメーター」を全部消してますけど、ちゃんとやれば次のようなレスポンスが返ってきます。
HTTP/1.1 200 OK Date: Tue, 07 Sep 2010 06:25:30 GMT Server: hi Status: 200 OK X-Transaction: 0000000000-00000-00000 X-RateLimit-Limit: 350 ETag: "4f05766016563d89d76886504b8e9a39"-gzip Last-Modified: Tue, 07 Sep 2010 06:25:30 GMT X-RateLimit-Remaining: 349 X-Runtime: 0.13102 Content-Type: application/json; charset=utf-8 Pragma: no-cache X-RateLimit-Class: api_identified X-Revision: DEV X-RateLimit-Reset: 1283844330 Set-Cookie: k=???; path=/; expires=Tue, 14-Sep-10 06:25:30 GMT; domain=.twitter.com Set-Cookie: guest_id=0000; path=/; expires=Thu, 07 Oct 2010 06:25:30 GMT Set-Cookie: lang=en; path=/ Set-Cookie: _twitter_sess=????; domain=.twitter.com; path=/ Vary: Accept-Encoding Content-Encoding: gzip Connection: close
例によってクッキーとかセッション ID とか全部消してますけど、このレスポンスにある X-RateLimit-Limit ヘッダに「最大 API 呼び出し回数」が、X-RateLimit-Remaining ヘッダに「残り回数」が入ってます。上の例だと 349 / 350 回、所定の API が呼び出せまっせ、ということになります。
で X-RateLimit-Reset ヘッダにある時間(値は 1970/1/1 0:00:00 からの経過秒数)に X-RateLimit-Remaining がリセットされますよ、ということもわかります。
ところが、Twitter だと OAuth 認証に必要な「正しいパラメーター」を違う渡し方をしても通ってしまうようなんですね。
その渡し方とは、次のように「正しいパラメーター」を GET の QUERY_STRING に渡してしまうというもの。
GET /1/statuses/home_timeline.json?count=100&oauth_consumer_key=????&oauth_nonce=????&oauth_signature_method=HMAC-SHA1&oauth_timestamp=0000000000&oauth_token=00000000-??&oauth_verifier=0000000&oauth_version=1.0&oauth_signature=???? HTTP/1.1 User-Agent: boeboe Content-Type: application/x-www-form-urlencoded Host: api.twitter.com Connection: keep-alive
よく見ると oauth_verifier という上の例には付いてないパラメーターが付いてますけど、これは OAuth 認証手順の最初のほうにある「アクセストークンの取得」に必要なものですね。つまり API に渡すパラメーターにプラスして OAuth 認証のアクセストークン取得パラメーターを付けてるわけです。
これは POST のリクエストボディを使っても同様のようです。上の例では GET 使ってるので Content-Type ヘッダは不要ですけど、コピペしたら付いてきたのでそのままにしてるだけです。
こんな「間違った」やり方でも普通に自分のアカウントとして認証されて、レスポンスも全く問題なく得られるんですけど、その内容が少し違ってます。
HTTP/1.1 200 OK Date: Tue, 07 Sep 2010 06:24:33 GMT Server: hi Status: 200 OK X-Transaction: 0000000000-00000-0000 X-RateLimit-Limit: 150 ETag: "c532a31f7aecfca1c532c63ad654275d" Last-Modified: Tue, 07 Sep 2010 06:24:31 GMT X-RateLimit-Remaining: 149 X-Runtime: 1.42691 Content-Type: application/json; charset=utf-8 Pragma: no-cache X-RateLimit-Class: api X-Revision: DEV X-RateLimit-Reset: 1283844271 Set-Cookie: k=????; path=/; expires=Tue, 14-Sep-10 06:24:31 GMT; domain=.twitter.com Set-Cookie: guest_id=????; path=/; expires=Thu, 07 Oct 2010 06:24:31 GMT Set-Cookie: lang=en; path=/ Set-Cookie: _twitter_sess=????; domain=.twitter.com; path=/ Vary: Accept-Encoding Connection: close
このレスポンスだと X-RateLimit-Limit ヘッダが 150 に、X-RateLimit-Remaining ヘッダが 149 になってます。つまりあと 149 回所定の API 呼び出しができるってことで、最初の例と違う値になってるんですね。
タイムスタンプはいじってないので、どちらも続けて送ったリクエストです(実はふたつめの例のほうが先に実行されてるのもバレバレ)。
両者の数字の減り方は独立してて一方が 349/350 でもう一方が 149/150 だから、この 2 種類を使い分ければ 498/500 の API 呼び出しが可能になってしまう、と。
Twitter の OAuth 認証は API 呼び出し回数にアカウント毎の制限をはかけるのも目的のひとつっぽいですが、本来 350 回のところプラス 150 回余計に TL 取得したりユーザー情報見たりできるわけですよ奥さん!
これは普通に exploit だろ...常識的に考えて...。
なのでこれを利用して何かしら不利益を被ってもわたしは知りませんから試したければ自己責任でどうぞだし、Twitter に報告したいけど英語でこれを説明するの骨が折れるのでまだやってないわけだし、周辺調査とかもしてないから既知の現象かどうかも知らないだしし!
あと関係ないけど API コールでも cookie 喰わせようとしてるってことは、クライアント的には cookie 食べた振りしたほうがサーバーに対して優しい振る舞いになったりするんですかね。
Psychs
> こんな「間違った」やり方
RFC 5849 に、きちんと規定されている正規の方法です。
3.5.2. Form-Encoded Body
3.5.3. Request URI Query
http://tools.ietf.org/html/rfc5849#section-3.5.2
http://tools.ietf.org/html/rfc5849#section-3.5.3
Mocel
ほんとだ、RFC の 3.5 章に Authorization ヘッダかリクエストのエンティティボディか、URI の query かのどれか使えってある!
トークン取得時だけ OK ってわけじゃなかったんですね…不勉強が露呈して恥ずかしい。
わざわざ教えてくだすってありがとうございます!
そうすると API の Rate Limit が一致しないおかしさが余計に際立ってきたので調べ直してみたら、API の呼び出し方で違いがあるっぽいことがわかってきました。
やばい、この記事ガセネタっぽい…。
MocelからPsychsへの返信
P.S Lime Chat は Ver.1 の頃から愛用させてもらってます。