HTTPとHTTPSを併用しログイン後の画面遷移はHTTPSしか許可しないサイトでの安全なセッション管理その2
「ログイン時にSecure属性付きのセッションCookieを再発行する実装の実現性」をASP.NETにおいて検証しました。
ASP.NET1.1
ASP.NET1.1でセッション破棄の処理を実装する場合には、Session.Abandonメソッドを使用します。ここでSession.Abandonを使用したセッションの再発行には、
- Session.Abandonメソッドを使用してもCookieの削除は行われない
- セッションIDは再利用される
という特性があります。セッションIDは再利用されるという特性が致命的なため、実装の実現性は今のところないと考えています。(いろいろ考えれば見つかってくるかもしれませんが、単純な方法では無理でしょう)
ASP.NET2.0
セッションID再発行の仕組みが用意されているので、少なくともJavaと同様なコードを実装することで、可能だと思います。(未検証)
sessionState Element (ASP.NET Settings Schema) | Microsoft Docs
ASP.NET1.1は、まだまだ現役で稼動しているフレームワークであるため、なんらかの解決手段を検討する必要があるでしょう。
ASP.NET1.1におけるHTTPとHTTPSを併用したセッション管理の仕組みとしては、認証Cookieの使用が推奨されています。セッション管理に使用するCookieと認証状態の管理に使用するCookieが独立して存在するため、認証CookieにSecure属性を設定することで、認証後のセッションデータを保護することが出来るという概念です。
この仕組みが安全に動作するならば、ログイン時にSecure属性付きのセッションCookieを再発行することが難しくても、サイトの安全性を守ることが可能となります。
HTTPとHTTPSを併用しログイン後の画面遷移はHTTPSしか許可しないサイトでの安全なセッション管理
HTTPとHTTPSを併用するサイトでの安全なセッション管理 - masaのメモ置き場 の続き
おっと、ログイン後の画面遷移はHTTPSしか許可しない前提だったら、ログイン時にSecure属性付きのセッションCookie再発行してあげれば終わりですね。前提がごっちゃになってました、ごめんなさい。
ただ、コンテナに依存しない実装を考えた場合、なかなか難しいですよ・・・
ServletだとセッションCookieの発行はコンテナの役目。従ってセッションCookieのフィールドを操作するインタフェースが公開されていないのです。コンテナの設定で対応しようとした場合、HTTPでのアクセス時にはSecure属性無しのCookieを、HTTPSでのアクセス時にはSecure属性有りのCookieを発行するように実装されていないとダメですが、実際のところどうなんでしょう??少なくとも完全にコンテナ依存の実装となってしまいます。
強引に実装するならば、以下のようなコードとなりそうです。
if (login(userID, password)) { HttpSession session = request.getSession(); Enumeration e = session.getAttributeNames(); Map buff = new HashMap(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); buff.put(key, session.getAttribute(key)); } session.invalidate(); session = request.getSession(true); Iterator ite = buff.keySet().iterator(); while (ite.hasNext()) { String key = (String) ite.next(); session.setAttribute(key, buff.get(key)); } Cookie cookie = new Cookie("JSESSIONID",new String(session.getId())); cookie.setSecure(true); cookie.setPath(request.getContextPath()) response.addCookie(cookie); }
この実装でのHTTPレスポンスヘッダはこれ
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Set-Cookie: JSESSIONID=27A8ADF8B02893ECF0C8BA5C22B89624; Path=/WebAppSec Set-Cookie: JSESSIONID=27A8ADF8B02893ECF0C8BA5C22B89624; Path=/WebAppSec; Secure Content-Type: text/html;charset=Shift-JIS Content-Length: 378
ちょっとおすすめしたくないですが、しょうがないのかなって感じです。
HTTPとHTTPSを併用するサイトでの安全なセッション管理
12/12 22:40 こっちが正解?自信なし・・・
考慮すべきは、
- HTTPのページにアクセスするとセッション情報が奪われてしまう可能性がある
- HTTPのページにアクセスするとセッション情報が操作されてしまう可能性がある
- 2重ログインでセッション情報が奪われてしまう可能性がある
の3点か?訳わかんなくなってきたので、また今度まとめなおし。。
HTTPページへのアクセス時
・・・ HttpSession session = request.getSession(); if (session.getAtribute("AuthTicket") != null) { Enumeration e = session.getAttributeNames(); Map buff = new HashMap(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); buff.put(key, session.getAttribute(key)); } session.invalidate(); session = request.getSession(true); Iterator ite = buff.keySet().iterator(); while (ite.hasNext()) { String key = (String) ite.next(); session.setAttribute(key, buff.get(key)); } } ・・・
ログイン処理
・・・ if (checkAccount(userId, password)) { HttpSession session = request.getSession(); Enumeration e = session.getAttributeNames(); Map buff = new HashMap(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); buff.put(key, session.getAttribute(key)); } session.invalidate(); session = request.getSession(true); Iterator ite = buff.keySet().iterator(); while (ite.hasNext()) { String key = (String) ite.next(); session.setAttribute(key, buff.get(key)); } session = request.getSession(true); String authTicket = generateAuthTicket(); session.setAttribute("AuthTicket", authTicket); Cookie authCookie = new Cookie("AuthCookie", authTicket); authCookie.setSecure(true); authCookie.setPath(request.getContextPath()); response.addCookie(authCookie); //認証成功 } else { //認証失敗 } ・・・ } private boolean checkAccount(String userId, String password) { boolean result = アカウントチェック; return result; } private String generateAuthTicket() throws NoSuchAlgorithmException { SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); byte[] b = new byte[32]; random.nextBytes(b); return toHexString(b); } private String toHexString(byte bytes[]) { StringBuffer sb = new StringBuffer(bytes.length * 2); for (int index = 0; index < bytes.length; index++) { int i = bytes[index] & 0xff; if (i < 0x10) { sb.append("0"); } sb.append(Long.toString(i, 16)); } return sb.toString(); }
HTTPSページへのアクセス時(ログイン後)
・・・ Cookie[] cookies = request.getCookies(); if (cookies != null) { Cookie authCookie = null; String authTicket= null; for (int i = 0; i < cookies.length; i++) { if (cookies[i].getName().equals("AuthCookie")) { authCookie = cookies[i]; authTicket = authCookie.getValue(); } } HttpSession session = request.getSession(); if (authTicket != null && authTicket.equals(session.getAttribute("AuthTicket"))) { //認証成功 } } //認証失敗 ・・・
12/12 20:40 ん、、、これじゃ駄目かも。もうちょい考えないと・・・
HTTPとHTTPSを併用する必要があるサイトでのセッション管理手法について、
SSLの関係で2つのセッションIDを使い分ける場合について|freeml byGMO
この辺りの議論とかコードとかを参考にさせて頂きながら考えたことをコードに落としてみました。
前提条件として、ログイン後の画面遷移はHTTPSしか許可しないことにしています。ここを許可しちゃうと今のところ安全な実装方法が分からないので。
#12/12 9:30 ちょっとコード修正
ログイン前までの遷移
普通にセッション変数を使用したりして実装
ログイン処理
・・・ if (checkAccount(userId, password)) { HttpSession session = request.getSession(); Enumeration e = session.getAttributeNames(); Map buff = new HashMap(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); buff.put(key, session.getAttribute(key)); } session.invalidate(); session = request.getSession(true); Iterator ite = buff.keySet().iterator(); while (ite.hasNext()) { String key = (String) ite.next(); session.setAttribute(key, buff.get(key)); } session = request.getSession(true); String authTicket = generateAuthTicket(); session.setAttribute("AuthTicket", authTicket); Cookie authCookie = new Cookie("AuthCookie", authTicket); authCookie.setSecure(true); authCookie.setPath(request.getContextPath()); response.addCookie(authCookie); //認証成功 } else { //認証失敗 } ・・・ } private boolean checkAccount(String userId, String password) { boolean result = アカウントチェック; return result; } private String generateAuthTicket() throws NoSuchAlgorithmException { SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); byte[] b = new byte[32]; random.nextBytes(b); return toHexString(b); } private String toHexString(byte bytes[]) { StringBuffer sb = new StringBuffer(bytes.length * 2); for (int index = 0; index < bytes.length; index++) { int i = bytes[index] & 0xff; if (i < 0x10) { sb.append("0"); } sb.append(Long.toString(i, 16)); } return sb.toString(); }
ログイン後の遷移
・・・ Cookie[] cookies = request.getCookies(); if (cookies != null) { Cookie authCookie = null; String authTicket= null; for (int i = 0; i < cookies.length; i++) { if (cookies[i].getName().equals("AuthCookie")) { authCookie = cookies[i]; authTicket = authCookie.getValue(); } } HttpSession session = request.getSession(); if (authTicket != null && authTicket.equals(session.getAttribute("AuthTicket"))) { //認証成功 } } //認証失敗 ・・・
副作用もないと思うので、業務でも使えるコードになっているはず・・・
結構長いコードになっちゃいますが、ここまでしないと安全なセッション管理は無理かなーと思っています。
PreparedStatementを使用したSQLInjection対策
エスケープだけしてれば、セキュリティ対策が万全になる訳ではないですよ - masaのメモ置き場
の話について、高木さんより「PreparedStatementを使いなさい、動的パラメータの例は分岐で処理しなさい」、という趣旨のコメントを頂いてしまった。まだ、もやもや感があるので、もう少し考えてみる。
数値型チェックの話
select password from usertable where id = 入力値 (idが数値型) 入力値に 1 or 1 = 1 が与えられると・・
数値型の列に数値型以外の値が渡された時に発生する問題は、どの層ならば対策可能で、どの層が責任を持つべきなの?という話。型の概念を持つ言語なら、データベース層に数値型の引数を受け付けるインタフェースが用意されていて、アプリケーション層ではそのインタフェースを呼び出すことになる。型変換はアプリケーション層が行うことになるので、対策するのもアプリケーション層になると思ったのだけれど、なにかが違うのかもしれない。
あと、型の概念がない言語では危ないんじゃないかな?とも、思って書いたのだけれど、そもそも型の概念がない言語をあまり触ったことがないので、よくわからないことに気付いてしまった。この問題は、もうちょっと勉強してから考え直すことにする。
分岐で処理する
select * from usertable order by 動的パラメータ 動的パラメータに入力値をそのまま使用していると・・・
この例は、JavaのPreparedStatementを意識して書いてみた。動的パラメータとして列名と昇順、降順のに結びつくパラメータが渡される場合には、ホワイトリスト的なアプローチが必要。という趣旨のことを書いたのだが、分岐で処理するという言葉の方が確かに正しい。
ホワイトリスト的アプローチという記述だと
許可する列名:id,name 許可する並び替えの値:ASC,DESC select * from usertable order by 許可された列名 許可された並び替えの値
と、いうイメージに結びつくが(入力値をそのまま使わないとしても)、これだとPreparedStatementが持つ準備済みSQLという意味にあまりそぐわない気がする。動的に生成しては、準備済みSQLではなくなってしまう。
それよりも
select * from usertable order by id ASC select * from usertable order by id DESC select * from usertable order by name ASC select * from usertable order by name DESC
準備済みSQLで対応出来ない例
では、どんなケースでも準備済みSQLを用意しておくことが出来るのか?と言われたらそうとは言えないだろう。
select * from usertable where id in ( 動的パラメータ ID のリスト )
この例だと、動的パラメータ ID の数が不確定なため、準備済みSQLを用意することが出来ない。パラメータの数に対応したSQLをあらかじめ
パラメータが1つの時:select * from usertable where id in (?)を使ってパラメータバインド パラメータが2つの時:select * from usertable where id in (?,?)を使ってパラメータバインド パラメータが3つの時:select * from usertable where id in (?,?,?)を使ってパラメータバインド 以下永遠に続く
みたいな形で「準備」しておくという対応は乱暴すぎるだろう。従って、 動的パラメータ ID のリスト は自作の関数などによって展開する必要がある。自作の関数で処理するということになると、SQLInjection対策はPreparedStatementを使えばよい、の一言で片付けてよい問題では無くなってくる。
バインドメカニズムを利用したSQLInjection対策
自作の関数を実装するには厄介な問題がある。文字列型ならば、'を''にエスケープする関数を作成すればよいという訳でなく、\をエスケープシーケンスとして認識するDBMS実装依存の問題も考慮しなければならない。従って、エスケープ関数を自作という対応は出来るだけ避けるべきであろう。エスケープ関数を自作しない解決策としは、プレースホルダを動的に生成する関数を作成するという対応が考えられる。
引数として渡された動的パラメータIDの数が4個ならば、?,?,?,?という戻り値を返す関数を作成すれば、
select * from usertable where id in (?,?,?,?)
というSQLを動的に生成し、パラメータをバインドすることが可能となる。*1ユーザ入力値をそのまま使用して構文を作成していないため、安全性も確保されている。この例はSQLを動的に生成しているので、準備済みSQLを使用しているとは呼べないかもしれない。ただ、パラメータをバインドする仕組みは使用しているので、バインドメカニズムを利用したSQLInjection対策と呼ぶことが出来るだろう。
エスケープだけしてれば、セキュリティ対策が万全になる訳ではないですよ
本題
「サニタイズ言うなキャンペーン」私の解釈
読みました。分かりやすくとてもよい記事だと思いますが、ホワイトリスト型のアプローチが補助的な対策であるとも読み取れてしまうこともありそうなため、エスケープだけしておけば大丈夫という誤った認識を持ってしまう開発者さんが増えてしまいそうなのが心配です。ということで、いちお書いておきます。
SQLの特殊文字をエスケープすることで対策可能なセキュリティの問題は、特殊文字が混在することにより構文が破壊されるといった限定された状況だけです。
例としては、SQLインジェクションの解説でよく取り上げられる
select password from usertable where id = '入力値' 入力値に ' or '1' = '1 が与えられると・・・
といった攻撃は防ぐことは出来ますが、
select password from usertable where id = 入力値 (idが数値型) 入力値に 1 or 1 = 1 が与えられると・・・
といった攻撃を防ぐことは出来ません。このケースでは、前もって入力値が数値型であることを確認しておかなければなりません。データベース層のロジックは数値型しか受け付けない仕様を持つべきで、数値型であることの確認(と数値型への型変換)はアプリケーション層が担うべき責務となります。アプリケーション層では、数値型の入力値しか受け付けないというホワイトリスト型のアプローチが必要となります。
他に特殊文字のエスケープだけでは足りないケースとしては検索系のシステムでは一般的な
select * from usertable order by 動的パラメータ 動的パラメータに入力値をそのまま使用していると・・・
といった構文のSQLがあげられます。このケースでは、Javeにおける PreparedStatement + setString() といったパラメータバインドの仕組みでも対応を完結することが出来ません*1ので、アプリケーション層における入力値チェック(及び入力値の変換)というアプローチが必要となります。
ライブラリなどの下位層で全ての問題が解決出来ることが理想ですが、残念ながら現状はそうではありません。それぞれの層における責務と出来ること出来ないことを正しく認識し、対策を行う必要があります。
ASP.NETのおかしな仕様
HTMLでエスケープしなければならない文字を(デフォルトでは) 入力の段階で弾いてしまうASP.NETは、そもそもその発想からして間違っている、と私は思う
この話は、http://vsug.jp/tabid/63/forumid/56/postid/789/view/topic/Default.aspx において私も議論しました。やっぱりおかしいですよね^-^;
*1:出来る場合は教えて下さい
文字コードとHTMLエンコードとCross-Site Scriptingの微妙な関係の考察
ちょっと古いけど、
2006-03-27
2006-03-28
ここら辺の問題を、Servlet(Tomcat5.5)を使っていろいろ試してみました。
public class BreakQuote extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { doPost(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // request.setCharacterEncoding("Shift-JIS"); // response.setCharacterEncoding("Shift-JIS"); // response.setContentType("text/html; charset=Shift_JIS"); String id = request.getParameter("id"); PrintWriter out = response.getWriter(); out.println("<html><body>"); out.println("<form method=\"POST\">"); out.println("<input type=\"text\" name=\"id\" value=\"" + id + "\">"); out.println("<input type=\"submit\" />"); out.println("</form>"); out.println("</body></html>"); } }
こんな感じでコードを書くと動きます。直感的には、Javaだと動かないんじゃね?って思ってたんですが、文字コード変換のメソッドを通過していない場合は動くみたいです。逆にコメントアウトしてあるコードのどれかを通過すると動きません。jspでは暗黙的にresponse.setContentType()が呼び出されるので動かないみたいです。
Accept-Language=ja 向けのシステムでは文字コード周りをしっかり設定しないと使い物になりませんが、Accept-Language=en 向けのシステムはそういう訳でもないため、注意が必要かもしれません。
RefererによるCSRF対策の根拠
高木浩光@自宅の日記 - 暗黙的に形成する事実標準の話と回避策の話を混同してはいけない, 駄目な技術文書の見分け方 その1の2
この前書いたのは、読み違えでぼろぼろだったので、もう一度よく考えてみました。
つまり、リンクをクリックしてのジャンプかどうかは規定にないものの、ジャンプ先のURLが書かれているページ がURLとして存在していない限り、Refererを送信してはいけない、つまり、少なくとも、任意のURLを Referer として送信することはしてはいけないことになっている。
RFC2616の記述から、「少なくとも、任意のURLを Referer として送信することはしてはいけないことになっている。」の結論をMust Notの動作として導き出していることに疑問があります。
(例えばユーザのキーボード入力のような)という補足が示すとおり「それ自身のURIを持たない出所から得られたもの」 の記述は、ブラウザのアドレスバーの直打ちや、ブックマークからのアクセス時の挙動に関する定義であるでしょう。このケースでは、セッションIDがURLに含まれていた場合にセッションIDが漏洩する、といったセキュリティ上の問題が発生する可能性があります。同じくRFC2616におけるRefererに関するセキュリティ上の問題点に関する記述をみても、そういった可能性を考慮していることが分かると思います。
この記述を、
- 存在しないURIからはRefererを送信してはならない
- 従って存在しないURIをRefererとして送信してはならない
- 存在しないURIが設定出来るため、Refererには任意の値を設定出来てはならない
という論法で拡大解釈することは間違いであると考えます。(ここは推測ですが・・・)
この解釈には、「Refererにはリンク元のURIのみしか設定してはならない」という前提が定義されている必要がありますが、RFC2616にはそのように記述されていません。リンク元とは異なるRefererが送られることはあきらかにおかしい挙動である為、省略された可能性も高いですが、Must Notの挙動として定義されているかどうかは別の問題であるでしょう。
従って、Refererを使用したCSRF対策が、「Refererに任意の値を設定出来てはならない」という前提の上に成り立っているのならば、論拠としては薄いのではないでしょうか?
「こういう定義がされているから、この機能を実装してはならない」という論拠に比べ
「こういう使い方をされることがあるから、この機能を実装してはならない」という論拠に説得力を感じることは出来ません。
以下補足
おおいわさんは、クロスドメインでヘッダ操作が可能であることに問題の根本があると考えているように見受けられたのですが、高木さんとは意見が違うのでしょうか?
「セキュリティ上問題があるから、クロスドメインでヘッダ操作が出来てはならない」という前提の上に成り立っているのならば、納得がいくのですが・・・
ただし、この場合は、RFCに従った実装の上でセキュリティの問題が発生することを証明する必要があると思います。また、ヘッダ操作が可能なプラグインの開発者はセキュリティ上の問題が発生しないことを考察した技術文書を公開するなどの慎重な対応を取ったうえで、機能をリリースすべきであるとも思います。Flashは残念なことに、このような技術文書を公開していません。
追記 11/19
はてなブックマークで
例が挙げられていると例以外のケースを除外するというご都合思考。「obtained from」を無視している。
という、酷評を頂きました。
「obtained from」を無視している。が何を意味しているのか良く分からずに困っていたら
The Referer field MUST NOT be sent if the Request-URI was obtained from a source that does not have its own URI, such as input from the user keyboard.
のownをRequest-URIに結び付けているんじゃ?って友達に教えてもらいました。
そうすると、Request-URIが、それ自身のURI(それ自身=Request-URI)を持たない出所から得られたもの(例えばユーザのキーボード入力のような)である場合には、Refererフィールドは送信してはならない。という訳となって、リクエスト送信元のページにRequest-URIが書かれていなければならないことになり、高木さんの主張の意味が分かります。
でも、ownはsourceにかかっていて、Request-URIが、(例えばユーザのキーボード入力のような)それ自身のURI(それ自身=リクエストの出所)を持たないソースから得られた場合には、Refererフィールドを送信してはならない。
という訳が正しいのではないかなーと思います。
(http://example.com/test.swf がURIを持たないと言っている?の可能性がありそうな気もします)
また、例が挙げられていると例以外のケースを除外するというご都合思考。
とも書かれてしまいました。
このように、前例に照らして、同一ホスト以外に任意のヘッダを送ることは、それを許すような製品があればそれは脆弱性であるというコンセンサスが業界にあると判断した。
前例をあげるが実は大量に報告されている脆弱性レポートを無視した文書の構成は、ご都合主義ではないのか?という疑問があります。
http://secunia.com/search/?adv_search=1&s=1&search=Header&w=0&vuln_title=1&vuln_software_os=1&vuln_bodytext=1&vuln_cve=1&critical%5B%5D=0&impact%5B%5D=9&where%5B%5D=0
(前例としてあげられている脆弱性レポートも、Refererのくだりは「問題があるとは思えない。」という返事に終わっていたり^-^; )https://bugzilla.mozilla.org/show_bug.cgi?id=302263#c5
なんか、よく分かりません。