Webエンジニア susumuis の技術ブログ

このブログの内容は個人の見解であり、所属する組織の公式見解ではありません

Androidの標準Webブラウザでセキュア属性付きのCookieの扱いがPCやiPhoneと異なる件について

Android用のWebサイトを作っていてはまったので、報告します。対象はブラウザ上で動くWebアプリの話で、ネイティブアプリではありません。
Googleで検索しても、ネイティブアプリ関連の情報は出てきますがWeb開発の情報が意外とすくなかったので、少しでもお役に立てれば幸いです。

概要

Android標準ブラウザ(Dolphinブラウザなども含む)で、セキュア属性付きのCookieの挙動がiPhoneのブラウザや、PCのChromeなどと異なります。

Cookieやセキュア属性の概要はこちらなどをご参照ください。http://itpro.nikkeibp.co.jp/article/COLUMN/20080221/294407/

PCやiPhoneのブラウザの場合、WebサーバがSet-Cookieレスポンスヘッダを返した場合、例え、そのCookieのセキュア属性が設定されていても、ブラウザはそのCookieを受け取ります(通称:食べます)。Chrome13、iOS 4.3.4で確認しました。
ところが、AndroidのWebブラウザでは、保持しているCookieにセキュア属性が付いている場合、HTTPSによって暗号化されたレスポンス以外ではCookieを受け取りません。Nexus S(Android 2.3)、Xperia Arc(Android 2.3)、HTC Desire(Android 2.2)で確認しました。

検証

再現コードは以下の通りです。確認するためには、自前でSSL付き(オレオレ証明書でも良い)のアプリサーバを立てることが必要です。誰かがappengineで立ててくれると便利ですね!(←お前がやれかw)

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		
	try {
		String path = request.getRequestURI().substring(request.getContextPath().length());
		if (path.equals("/cookie")) {
			System.out.println("start");
			String val = String.valueOf(new Date().getSeconds())
			Cookie cookie = new Cookie("TEST1", val);
			Cookie cookie2 = new Cookie("TEST2", val);
			cookie.setSecure(true);
			response.addCookie(cookie);
			response.addCookie(cookie2);
			
			response.setContentType("text/plain; charset=UTF-8");
			PrintWriter writer = response.getWriter();
			writer.println("request cookies: ");
			if (request.getCookies() != null) {
				for (Cookie reqCookie : request.getCookies()) {
					writer.println("\t" + cookieToString(reqCookie));
				}
			}
			writer.println("response cookie: ");
			writer.println("\t" + cookieToString(cookie));
			writer.println("\t" + cookieToString(cookie2));
			
		}
	} catch (Exception e) {
		throw new ServletException(e);
	}
	}
	
	protected String cookieToString(Cookie cookie) {
		return cookie.getName() + "=" + cookie.getValue() + "\n\t\t" +
					"d: " + cookie.getDomain() +
					", p: " + cookie.getPath() +
					", v: " + cookie.getVersion() +
					", a: " + cookie.getMaxAge() +
					", c: " + cookie.getComment() +
					", s: " + cookie.getSecure();
	}
}

このようにすると、

request cookies:
TEST2=19
d: null, p: null, v: 0, a: -1, c: null, s: false
response cookie:
TEST1=46
d: null, p: null, v: 0, a: -1, c: null, s: true
TEST2=46
d: null, p: null, v: 0, a: -1, c: null, s: false

のように表示されます。これを、同じブラウザでタブを切り替えて、httpsでアクセスすると、

request cookies:
TEST1=1
d: null, p: null, v: 0, a: -1, c: null, s: false
TEST2=1
d: null, p: null, v: 0, a: -1, c: null, s: false
response cookie:
TEST1=27
d: null, p: null, v: 0, a: -1, c: null, s: true
TEST2=27
d: null, p: null, v: 0, a: -1, c: null, s: false

このようになります。HTTPSの時だけセキュアなTEST1クッキーが受け取れます。

この時、PCのブラウザやiPhoneでは、request cookiesの値は、HTTPでも、HTTPSでも前回リクエストした時のresponse cookieの値が設定されています。しかしながら、Androidのブラウザでは、

request cookies:
TEST1=26
d: null, p: null, v: 0, a: -1, c: null, s: false
TEST2=9
d: null, p: null, v: 0, a: -1, c: null, s: false
response cookie:
TEST1=44
d: null, p: null, v: 0, a: -1, c: null, s: true
TEST2=44
d: null, p: null, v: 0, a: -1, c: null, s: false

のように、TEST1とTEST2が異なる値を返してしまいます。TEST1に入っているのは「前回HTTPSでアクセスしたときにresponseされたcookieです」

実験の結果より、Androidの標準ブラウザでは、HTTPSでしか、セキュア属性付きのCookieは、受け取らないことがわかりました。ただし、これにも条件があって、端末側にまだ同じCookieが存在しない場合は、HTTPでもセキュアCookieを食べています。

つまり、AndroidiPhoneのブラウザは同じWebkitエンジンを使用していますが、セキュリティーポリシーに違いがあります。

もしかしたら、PCやiPhoneでも設定を変更するれば動作が変わるかもしれません(未確認)。しかし、標準状態で使用している人が多いですから、実質上上記の状態がWebの現状と考えてよいでしょう。

考察

AndroidのWebに対するセキュリティーポリシーは強力だと言えます。たとえ、端末やDNSサーバがクラックされて、Webサイトを偽装されたとしても、偽装のリスクがあるHTTP通信によって、より安全な(証明書がある)SSL通信に影響をあたえることができないからです。以前にもChromeで突然JavaScriptのセキュリティを厳しくされて今までのページが動作しないということがありました。(参照させて頂きます:http://blog.bitmeister.jp/?p=1734)

Googleギークたちはセキュリティ対策に非常に厳しい姿勢で望んでいると思います。たとえ一部の古いWebサイトを動かなくしたとしても、彼らはWebの安全性を大事にするのでしょう。現在のところは、PC版のChromeでは、今回の挙動はしていませんが、将来のChromeにこのような仕様変更が入ったとしても不思議ではないでしょう。

では、我々はWeb開発者どうしたら良いでしょうか?答えの一つは「もうCookieを使うのをやめよう」ではないでしょうか。HTML5など新しい技術を常に勉強して、セキュリティリスクが少なく、よりリッチで時代に即したサイトやWebサービスを作って行きましょうというのが、彼らからのメッセージなのかもしれません。
※こんな精神論を述べるのは、迷ったのですが、最終的に掲載することにしました。お目汚し失礼しました。

追記(2012/03/03)

半年前のエントリに今頃ブクマコメントがをいただいて驚きました。こんな辺鄙なブログを閲覧いただき誠にありがたく思います。

ご指摘を頂いたとおり、Secure属性付きのCookieをWebサーバが送信していることに問題があります。これからWebアプリケーションを開発される際はそのことを厳守するべきです。
しかし、この望ましくない動作を期待していた古いアプリケーションが実際に存在し、Androidブラウザで動かないということで調査と実証、および対策をしたのが本エントリになります。

iPhoneやPCのChromeはこれら過去への互換性を優先し、Android標準ブラウザはあるべき姿を優先したと考察しましたが、この状況は記事を書いた昨年9月時点のものであり、今のiPhone5, Android4.xなどでは動作が異なる可能性もあります。

Cookieを使わない」は言い過ぎでした。いわゆるHTML5Webアプリケーションの可能性は感じていますが本件とは直接関係がありません。

コメントを頂いた「Cookieを使わない」の具体例については、個人情報などは別にいわゆるAjax経由で取得する方法が考えられます。クロスドメインが使用できるXHR2を利用すれば、アプリケーションサーバと個人情報の格納先を分離することができます。ページ間をまたぐ場合はWebStorageを活用することで、Cookieの代替とすることができます。ただ、Cookieと同じようにこちらも攻撃者からの脆弱性を考慮する必要性は存在し、ある程度運用方法が固まったCookieを正しく使うことに比べて、まだリスクがあるかもしれません。