<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>WAF Tech Blog</title>
<link rel="alternate" type="text/html" href="http://www.scutum.jp/information/waf_tech_blog/" />
<link rel="self" type="application/atom+xml" href="http://www.scutum.jp/information/waf_tech_blog/atom.xml" />
<id>tag:www.scutum.jp/information/waf_tech_blog/</id>
<updated>2011-10-25T10:39:46Z</updated>
<subtitle>SaaS型WAF「Scutum（スキュータム）」の開発者/エンジニアによるブログです。“WAFを支える技術”をテーマに幅広く、不定期に更新中！</subtitle>
<entry>
<title>Tomcatなど、Java製のサーバでBEAST対策を行う方法</title>
<link rel="alternate" type="text/html" href="/information/waf_tech_blog/2011/10/waf-blog-009.html" />
<id>WTB-16_20111026130000</id>
<published>2011-10-26T04:00:00Z</published>
<updated>2011-10-26T04:00:00Z</updated>
<summary>CBCモードへの選択平文攻撃を扱った前々回のエントリ、そしてBEASTの全体像について解説した前回のエントリに続き、今回はTomcatなどのJava製のサーバアプリケーションにおいて、BEAST対策を実施する方法について見ていきます。なお、本エントリの対象はOracleのJava1.6系となります。他のJava実行環境やJSSE実装では事情が異なる可能性があります。</summary>
<author>
<name>金床　“Kanatoko”</name>
</author>

<content type="html" xml:lang="ja" xml:base="http://www.scutum.jp/information/waf_tech_blog/">
<![CDATA[<h3>はじめに</h3>
<p>CBCモードへの選択平文攻撃を扱った<a href="/information/waf_tech_blog/2011/10/waf-blog-007.html" target="_blank">前々回のエントリ</a>、そしてBEASTの全体像について解説した<a href="/information/waf_tech_blog/2011/10/waf-blog-008.html" target="_blank">前回のエントリ</a>に続き、今回はTomcatなどのJava製のサーバアプリケーションにおいて、BEAST対策を実施する方法について見ていきます。なお、本エントリの対象はOracleのJava1.6系となります。他のJava実行環境やJSSE実装では事情が異なる可能性があります。</p>
]]><![CDATA[<h3>Cipher Suiteの選択が鍵</h3>
<p>SSLの通信では、まずクライアント側が暗号化に使いたいCipher Suitesの候補の一覧を送信し、サーバ側はその中から1つ選ぶという動作となります。例として、Google ChromeのSSL通信をWiresharkでパケットキャプチャしたときのイメージを示します。</p>
<img src="/information/waf_tech_blog/images/clienthello.png" /><br>
<p>ClientHelloメッセージの中で、36ものCipherSuiteのリストが送信されていることがわかります。</p>
<p>次に、サーバ側から送られるServerHelloメッセージの中身を見てみます。</p>
<img src="/information/waf_tech_blog/images/serverhello.png" /><br>
<p>下から2行目から、今回はサーバ側がTLS_RSA_WITH_AES_128_CBC_SHAを選択したことがわかります。</p>

<h3>Javaではサーバ側でCipherSuiteの優先順位を決定することができない</h3>
<p>サーバ側でのBEAST対策として、CipherSuiteにRC4のようなストリーム暗号を使用するものを選ぶという方法があります。しかし残念なことに、TomcatのようなJavaで書かれたサーバアプリケーションでは、クライアント側から送られるCipherSuiteの一覧が先頭から順番に評価され、サーバ側で有効にしているものが見つかった時点で、そのCipherSuiteが選択されます。そのため、サーバ側がAESとRC4のどちらも有効になっており、かつクライアント側がRC4よりもAESを使いたがっている場合には、必ずAESが選択されてしまいます。</p>

<p>該当部分のソースコードはcom.sun.net.ssl.internal.sslパッケージのServerHandshakerクラス内にあります。以下のchooseCipherSuiteという関数内でCipherSuiteが決定されます。</p>
<pre>
/*
 * Choose cipher suite from among those supported by client. Sets
 * the cipherSuite and keyExchange variables.
 */
private void chooseCipherSuite(ClientHello mesg) throws IOException {
    for (CipherSuite suite : mesg.getCipherSuites().collection()) {
        if (isEnabled(suite) == false) {
            continue;
        }
        if (doClientAuth == SSLEngineImpl.clauth_required) {
            if ((suite.keyExchange == K_DH_ANON) || (suite.keyExchange == K_ECDH_ANON)) {
                continue;
            }
        }
        if (trySetCipherSuite(suite) == false) {
            continue;
        }
        return;
    }
    fatalSE(Alerts.alert_handshake_failure,
                "no cipher suites in common");
}
</pre>

<p>関数先頭のforループが、ClientHelloメッセージに含まれるCipherSuite一覧に対して行われていることがわかります。このようにソースコード内において完全にクライアント側を優先する実装となってしまっているため、設定項目の変更や起動オプションなどによってサーバ側の挙動を変更することはできないことがわかります。</p>
<p>サーバ側では、SSLSocketクラスのsetEnabledCipherSuitesメソッドを使うことにより、CipherSuiteの有効・無効については制御することができます。そのため、AESのようなブロック暗号を使用するCipherSuiteをすべて無効にしてしまうという方法も技術的には可能ですが、この場合実質的にRC4のみをサポートするSSLサーバとなってしまうでしょう（現実的にはRC4をサポートしないクライアントというのはあまりいないかと思われますので、それでも問題ないと考えることもできるかもしれません）。</p>

<h3>JSSEをハックする</h3>
<p>それでは、ここから何ができるか考えてみることにします。現実的には、次のような動作をするSSLサーバが望ましいでしょう。</p>
<ul>
<li>クライアント側がAES等とRC4のどちらもサポートしている場合には、その順番によらず、RC4を使用する</li>
<li>クライアント側がAES等をサポートし、RC4をサポートしない場合には、AES等を使用する</li>
</ul>

<p>これを実現するために、JSSE実装に手を入れ、ServerHandshakerクラスの挙動を変更します。ソースコードを書き換えてコンパイルし、クラスファイルを生成してからデフォルトの実装と入れ替えることで目的は達成できますが、ここでは別のアプローチを取ってみます。<a href="http://www.csg.is.titech.ac.jp/~chiba/javassist/" target=_blank>Javassistライブラリ</a>を利用して実行時にバイトコードを書き換えます。この方法のメリットとして、jsse.jarを書き換えずに済むため、Javaのバージョンアップの際に互換性の問題が出にくいという点があげられます。</p>

<p>chooseCipherSuite関数の中身を、次のようにサーバ側のCipherSuiteリストについてループするように書き換えます。</p>
<pre>
{
java.util.Collection c = enabledCipherSuites.collection();
java.util.Iterator p = c.iterator();
while( p.hasNext() )
  {
  com.sun.net.ssl.internal.ssl.CipherSuite suite = ( com.sun.net.ssl.internal.ssl.CipherSuite )p.next();
  if( !$1.getCipherSuites().contains( suite )
   || !suite.isAvailable()
    )
    {
    continue;
    }
  if (doClientAuth == com.sun.net.ssl.internal.ssl.SSLEngineImpl.clauth_required)
    {
    if ((suite.keyExchange.toString().equals( "DH_anon" ) ) || (suite.keyExchange.toString().equals( "ECDH_anon" ) ))
      {
      continue;
      }
    }
  if (trySetCipherSuite(suite) == false)
    {
    continue;
    }

  return;
  }
fatalSE( com.sun.net.ssl.internal.ssl.Alerts.alert_handshake_failure, "no cipher suites in common" );
}
</pre>
<p>Javassistで書き換えるためのソースコードであるため、ソースコード中のクラス名はパッケージ名を省略せずに記述しています。また、$1は1つめの引数(ClientHello mesg)を示します。</p>
<p>アプリケーションの起動に続き、書き換え対象のクラスがロードされる前にJavassistを使ってバイトコードを書き換えます。これは次のようなコードで行います。</p>
<pre>
private static void initServerHandShaker()
throws Exception
{
ClassPool pool = ClassPool.getDefault();
pool.get( "com.sun.net.ssl.internal.ssl.Debug" ).toClass();
pool.get( "com.sun.net.ssl.internal.ssl.Handshaker" ).toClass();
CtClass cc = pool.get( "com.sun.net.ssl.internal.ssl.ServerHandshaker" );
CtMethod method = cc.getDeclaredMethod( "chooseCipherSuite" );
String bodyStr = MStreamUtil.streamToString( MStreamUtil.getResourceStream( "net/jumperz/app/MCipherSuiteUtil/ServerHandshaker.txt" ) );
method.setBody( bodyStr );
cc.toClass();
}
</pre>
<p>コードは<a href="http://www.jumperz.net/tools/cipher_suite_util.jar">こちら(cipher_suite_util.jar)</a>からダウンロードできます（ライセンスはMozilla Public License 1.1です）。実行には別途Javassistライブラリが必要となります。</p>
<p>また、注意が必要な点として、JavassistはJRE/lib/以下にあるjarファイルについてはバイトコードの書き換えを行うことができないという点が挙げられます。そのため、実行する際にはjsse.jarを別の場所に移動した上で、別途クラスパスに含むようにする必要があります。</p>

<h3>使用方法</h3>
<p>前項で示したコードはアプリケーションの起動のごく早い段階で実行される必要があるため、別のアプリケーションをランチャーのように起動する、ブートストラップ用のクラスとしてまとめました。例として、書き換えたJSSEを使って起動したいアプリケーションのクラス名が次のようなものである場合を考えます。</p>
<pre>
jp.example.app1
</pre>
<p>起動は通常、次のように行われるものとします。アプリケーションに対して2つの引数(arg1 arg2)を与えています。</p>
<pre>
java jp.example.app1 arg1 arg2
</pre>
<p>バイトコード書き換えを行う場合、前述したようにjsse.jarを移動してクラスパスに含み、またcipher_suite_util.jarとJavassistライブラリがロードできる状態(クラスパスの指定や、JRE/lib/ext/以下に配置する等）にした上で、次のようにします(クラスパスの記述は省略しています)。</p>
<pre>
java net.jumperz.app.MCipherSuiteUtil.Bootstrap jp.example.app1 arg1 arg2
</pre>
<p>このようにすることで、必要なクラスファイルの書き換えを行った後に、アプリケーション本体のクラスのmainメソッドがarg1とarg2を引数として呼び出されます。</p>

<h3>Tomcatに適用する</h3>
<p>それでは実際の例として、Tomcatに適用する方法(Linuxの場合)を見ていきます。</p>
<p>Tomcatの起動や終了はあらかじめ用意されているシェルスクリプトで行うことが一般的です。本来、Tomcat起動時のクラスパスを指定するためにはシェルスクリプトを書き換える必要があります。しかしTomcatにはJSSE_HOMEという環境変数を指定することでjsse.jarが別の場所にあってもロードすることができる機能があるので、今回はこれを利用します。また、cipher_suite_util.jarについてもこの機能をうまく利用することでクラスパスを書き換える必要がなくなります。</p>
<p>/usr/local/jsse/lib/というディレクトリを作成し、そこにJRE/lib/jsse.jarを移動します。また、同時にcipher_suite_util.jarをjcert.jarという名前にリネームしてこのディレクトリに置いておきます。</p>
<pre>
/usr/local/jsse/lib/
                   +--jsse.jar
                   +--jcert.jar
</pre>
<p>次のように、環境変数JSSE_HOMEの値を/usr/local/jsse/に設定します(bashの場合)。"lib"は含まなくてよいので注意します。</p>
<pre>
export JSSE_HOME=/usr/local/jsse/
</pre>
<p>
また、JavassistのjarファイルをJRE/lib/ext/内に配置します。
</p>
<p>続いて、アプリケーションが起動される際に、javaコマンドに与えられるクラス名の部分を書き換えます。このためにはシェルスクリプトを直接書き換える必要があります。書き換える対象のファイルはcatalina.shになります。</p>
<p>書き換え前</p>
<pre>
org.apache.catalina.startup.Bootstrap "$@" start \
</pre>
<p>書き換え後</p>
<pre>
net.jumperz.app.MCipherSuiteUtil.Bootstrap org.apache.catalina.startup.Bootstrap "$@" start \
</pre>
<p>書き換える箇所は、通常は300行目付近になります（Securityマネージャなどを使っている場合には別の場所になります）。</p>
<p>このようにすることで、サーバ側でCipherSuiteを選択可能なTomcatを起動することができるようになります。</p>
<p>少々ややこしいので、手順を箇条書きでまとめると以下のようになります。</p>
<ul>
<li>/usr/local/jsse/lib/を作成する</li>
<li>JRE/lib/jsse.jarを/usr/local/jsse/lib/に移動する</li>
<li>cipher_suite_util.jarをjcert.jarという名前にして/usr/local/jsse/lib/に配置する</li>
<li>環境変数JSSE_HOMEの値を/usr/local/jsse/に設定する</li>
<li>JavassistのjarファイルをJRE/lib/ext/に配置する</li>
<li>catalina.shの書き換えを行う</li>
</ul>

<h3>まとめ</h3>
<p>このように、Javassistを利用することで、1.6系列のJavaにおいてサーバ側でCipherSuiteを選択することが可能となります。BEAST対策が現状サーバ側で必要とされるのかどうか、という点についてはさまざまな意見があるかと思いますが、「Javaサーバアプリケーションではそもそも対策できない」というのでは少々さみしい気がしますので、今回このようなエントリとしてまとめてみました。Guardian@JUMPERZ.NETのような、Java製のWAFアプリケーションなどでも、同じ方法を利用することができます。</p>
<p>BEASTについてのエントリはこの3つでひとまず区切りとなります。フィードバック等あればお気軽に<a href="http://twitter.com/kinyuka" target=_blank>@kinyuka</a>までご連絡ください。</p>
]]></content>

</entry><entry>
<title>BEAST(Browser Exploit Against SSL/TLS)とは何か</title>
<link rel="alternate" type="text/html" href="/information/waf_tech_blog/2011/10/waf-blog-008.html" />
<id>WTB-15_20111019130000</id>
<published>2011-10-19T04:00:00Z</published>
<updated>2011-10-19T04:00:00Z</updated>
<summary>CBCモードへの選択平文攻撃を扱った前回のエントリに引き続き、BEASTについて見ていきます。今回は、BEASTの全体像について解説します。なお、BEASTの実際の攻撃コードはパブリックにされていないため、この記事の内容はあくまでも推測に基づくものとなっていることをご了承ください。</summary>
<author>
<name>金床　“Kanatoko”</name>
</author>

<content type="html" xml:lang="ja" xml:base="http://www.scutum.jp/information/waf_tech_blog/">
<![CDATA[<h3>はじめに</h3>

<p>CBCモードへの選択平文攻撃を扱った<a href="/information/waf_tech_blog/2011/10/waf-blog-007.html" target="_blank">前回のエントリ</a>に引き続き、BEASTについて見ていきます。今回は、BEASTの全体像について解説します。なお、BEASTの実際の攻撃コードはパブリックにされていないため、この記事の内容はあくまでも推測に基づくものとなっていることをご了承ください。</p>
]]><![CDATA[<h3>BEAST以前の情報</h3>

<p>BEASTはSSLに対する「100%新しい画期的な攻撃方法」ではありません。過去に発見されていたいくつかのテクニックを適切に組み合わせ、さらに最後のひとさじを加えることで華麗にまとめあげたものとなっています。</p>
<p>BEASTの基となっている技術は、ほぼ次の2つの論文にまとまっていると考えられます。</p>
<p>
<a href="http://eprint.iacr.org/2004/111.pdf" target="_blank">The Vulnerability of SSL to Chosen-Plaintext Attack</a><br />

<a href="http://eprint.iacr.org/2006/136.pdf" target="_blank">A Challenging but Feasible Blockwise-Adaptive Chosen-Plaintext Attack on SSL</a>
</p>
<p>BEASTがこれら2つの論文と決定的に異なっているのは、その攻撃成功確率の高さです。「現実的な脅威と言えるかどうか？」という点で、BEASTは一歩ぬきんでた存在であると言えます。</p>

<h3>BEAST作者は何を発見したのか？</h3>

<p>上記の2つの論文では、<a href="/information/waf_tech_blog/2011/10/waf-blog-007.html" target="_blank">前回のエントリ</a>で触れた「CBCモードへの選択平文攻撃」を、SSLに対していかに行うか？が話題の中心となっています。さまざまな仮想的な条件の元で、どの程度の確率で攻撃が可能か、という点を追求しているものの、具体的に成功確率の高い攻撃モデルを提供することはできていませんでした。また、攻撃対象もあいまいなものとなっています。</p>
<p>BEAST作者の2人組が発見したのは、次の2点です。</p>
<p>1点目は、この攻撃を成功させる理想的な通信の発見です。それはHTTPSのリクエストであり、攻撃対象をCookieに絞ることで、前述した2つの論文が追い求めていた「現実的に可能な攻撃」となっています。</p>

<p>2点目は、暗号化通信のストリームにおいて、暗号化ブロックの境界位置をイヴが自由にずらすことで、常に1バイトずつのブルートフォース攻撃が可能になる、ということです。彼らはこれをBCBA(Blockwise Chosen-Boundary Attack）と呼んでいます。</p>
<p>続いて、これら2点が実際の攻撃においてどのように用いられているのか、具体的な攻撃のシナリオを見ていきます。</p>

<h3>具体的な攻撃シナリオ</h3>

<p>BEASTが想定する攻撃シナリオは、次のような前提となっています。</p>
<ul>
<li>ボブが運営するウェブサイトに、アリスがログインし、ウェブサイトを利用するタイミングを狙う</li>
<li>ログイン後のセッション管理はCookieで行われる</li>
<li>仮にイヴがアリスのCookieを入手すれば、アリスになりすまして、ボブのウェブサイトにログインすることができる</li>

<li>通信はすべてHTTPSで行われている</li>
<li>イヴはパケットキャプチャ能力を持っており、アリスとボブのサーバ間の通信をすべて傍受できる</li>
<li>イヴはパケットの傍受は可能であるものの、改ざんを行うことはできない</li>
<li>該当するHTTPSセッションにおいてCBCモードを使用する共通鍵暗号方式が採用されている</li>
</ul>
<p>イヴは、何かしらの方法で、あらかじめアリスのウェブブラウザにマルウェアを送り込んでおきます。実際のBEASTのデモで使われたのは、Javaアプレットだったようです。</p>
<p>アリスがボブのウェブサイトにログインした後が、イヴにとっての攻撃のタイミングとなります。アリスとボブのサーバ間の通信はすべてHTTPSであるため、イヴにとって、正確に「アリスがいつログインしたか」を判断することは難しいでしょう。しかしデモで使われたpaypal.comのように、基本的にログインしてから使うタイプのウェブサイトであれば、ある程度の量のパケットが飛んだ時点でログインしたと判断することができます。</p>
<p>イヴは攻撃を開始します。あらかじめ送り込んでおいたマルウェアをリモートで制御し、ボブのウェブサイトにHTTPSのリクエストを送信させます。このリクエストには自動的にCookieが付加され、このCookie部分を復号することがイヴの目的となります。</p>

<h3>CBCモードへの選択平文攻撃を開始</h3>

<p>TLS_RSA_WITH_AES_256_CBC_SHAのようなCipherSuiteが選択されていれば、CBCモードに対する選択平文攻撃を仕掛けることが可能となります。</p>
<p>イヴが操っているマルウェアはHTTPSリクエストを送信しますが、この際リクエストのメソッドやURIはイヴが自由にコントロールすることができると考えてよいでしょう。例えばPOSTメソッドで/AAAAAAというURIにアクセスさせるリクエストを考えます。AESではブロック長は16バイトとなるので、最初のブロックは次のようになるでしょう。</p>
<pre>POST /AAAAAA HTT
</pre>
<p>つまり、このブロックはイヴにとって完全に既知な内容となります。では、次のブロックはどうでしょうか。ここで、仮にボブのウェブサイトが「bob.example.jp」とし、リクエストヘッダの最初の行はHostフィールドが来ているものとします。すると次のようになります。</p>
<pre>P/1.1\r\nHost: bob
</pre>
<p>3ブロック目は次のようになります。(Hostフィールドに続いてUser-Agentフィールドが並んでいると仮定します)</p>
<pre>.example.com\r\nUs
</pre>
<p>このようにHTTPSリクエストは16バイトごとのブロックに分割されて暗号化されていきます。2ブロック目に注目した場合、上の例ではHostフィールドが先頭に来ていると仮定しましたが、これは必ずしもそうなるとは限りません。そのため、イヴにとって既知なのはリクエスト行の最後のCRとLFの並びまでであり、「Host」から先の部分は未知であると考えられます。つまり、2ブロック目は、イヴにとって既知の部分(7バイト)と未知のバイト(9バイト)から構成されています。</p>

<p>この場合、未知の部分が9バイトもあるため、ブルートフォース攻撃を成功させるためには最大で256の9乗もの回数の試行が必要とされ、実質的に攻撃することはできません。</p>
<p>そこで、イヴは2ブロック目の未知のバイトを1バイトだけにすることを考えます。URIを「/AAAAAA」ではなく、「/AAAAAA12345678」とするのです。すると、1ブロック目は次のようになります。</p>
<pre>POST /AAAAAA1234
</pre>
<p>2ブロック目は次のようになります。</p>
<pre>5678 HTTP/1.1\r\nH
</pre>
<p>2ブロック目において、イヴにとって未知なのは最後の1バイト(右端Hの部分)だけとなり、現実的にブルートフォース攻撃が可能となります。</p>
<p>イヴはPOSTリクエストのボディ部分において、適切にブロックの境界を判断し、自由にバイト列を組み立てます。16バイトのブロックごとに試行を重ね、最大で256回のチャレンジでこの「H」の文字を得ることができます。この試行はJavaアプレットとパケットキャプチャが連携して行います。前述したように暗号化前のデータはxorを経た、データとしてはまったくランダムなバイト列のようなものになるため、Javaアプレットのように送信するデータの形式に対して制限がゆるいものが攻撃に適しています。例えば送信するデータの（暗号化前の）形式について「正しいUTF-8の文字列であること」のような制限がある場合には、攻撃が困難なものとなることが予想されます。JavaScriptを使った攻撃などではこのような制限が存在することが想定されます。</p>
<p>もっとも攻撃効率が悪い場合でも、16x256=4096バイト、つまり約4KBほどのサイズのリクエストで未知の部分を1バイト得ることができます。実際には、アルファベットや数字などを優先することで、より効率の良いブルートフォース攻撃が可能となるでしょう。</p>
<p>1バイト判明したら、また同じようにURIの長さを調節することで、次の1バイトだけがブロックの最後の部分に位置するようにします。当然新しいHTTPSリクエストとなり、場合によっては(Keep-Aliveなどが関係します)新しい共通鍵が使われることもありますが、それはこの攻撃には影響を及ぼしません。リクエストに同じCookieが付加されている間は、HTTPヘッダの1文字目から順番にブルートフォース攻撃を行うことで、最終的にはCookie全体を取得することが可能となるのです。</p>

<p>また、HTTPSリクエストは同時並列的に送信することもできますので、より効率的にソフトウェアを作成することで、ブルートフォース攻撃の速度を飛躍的に高めることが可能となるでしょう。</p>

<h3>HTTPの性質が招いたBEAST</h3>

<p>通常わたしたちがウェブブラウザを使っているとき、バックグラウンドでどのようなHTTPリクエストがどのようなウェブサーバに飛んでいるかは、ほとんど気にされません。特にAjaxが一般化されてからは、ウェブサイト間の移動などのタイミングとは関係なく、数多くのHTTPリクエストが飛び交うようになりました。そのため、イヴがアリスのウェブブラウザ上のマルウェアから多くのHTTPSリクエストを送らせても、そのことがアリスに不自然に思われる可能性は極めて低いと考えられます。</p>
<p>イヴは数多くのHTTPSリクエストを送信させることができますが、これらのリクエストには毎回必ずCookieが含まれます。多くの場合、このCookieの内容は、数分から数十分程度、つまりセッションが切れるまでの間は、同じ値を持っていると考えられます。HTTPのKeep-Alive機能が使われる場合を除き、多くの場合、新たなHTTPSリクエストを送るたびに新しいSSL接続が生成されるため、暗号化に使われる共通鍵は異なるものとなってしまいますが、「CBCモードへの選択平文攻撃」においてはそれはそれほど重要なことではありません。</p>
<p>このように、以下に挙げるようなHTTP固有の性質が、まさにBEASTによる攻撃を可能にするものとなっています。</p>
<ul>
<li>イヴはアリスに知られずに、何度もメッセージを送らせることができる</li>
<li>メッセージには毎回Cookieが自動的に含まれる</li>

<li>URI部分やリクエストボディ部分など、イヴにとっての自由度が高い</li>
</ul>
<p>アリスに意図させずにイヴがリクエストを送信させ、そこに自動的にCookieが含まれてしまう点を攻撃するという意味では、BEASTはCSRF攻撃であるとも言えます。</p>

<h3>Javaアプレットのバグ</h3>

<p>BEASTの作者はHTTPSとCookieをターゲットにするということは比較的早い段階で決めたようですが、実際にデモを行うための実装に何を選ぶのかについては、紆余曲折があったようです。Javaアプレットをはじめ、FLASHやSilverlight、WebSocketやXMLHttpRequest等が検討された結果、最終的にはJavaアプレットが選ばれていますが、デモではJavaアプレットの未知の脆弱性が使用されています。</p>
<p>通常、イヴのサイトに置かれたアプレットからは、Same Origin Policy(SOP)に違反するため、ボブのサイトにリクエストを自由に送ることはできません。つまりBEASTはSSLの脆弱性を攻撃する、と喧伝されたものの、実際にはJavaプラグインの脆弱性に依存した形のものとなってしまっていました。そのため、「なんだ、結局はSSL自体の問題ではなく、Javaの脆弱性なのか」といった見方もする人もいたようです（私も、最初はそう思いました）。</p>
<p>ただし、これまでに見てきたように、たとえJavaプラグインに脆弱性がなかったとしても、BEASTの着眼点は実に巧妙であり、多数存在するウェブブラウザのプラグインアーキテクチャのどこかで、今後も攻撃を可能にするポテンシャルを秘めていると考えられます。</p>

<h3>対策</h3>

<p>AESのようなブロック暗号ではなく、RC4のようなストリーム暗号が優先して使われるように設定することで対策となります。Googleのウェブサーバでは（おそらくこちらはBEAST対策ではなくCPU使用率の問題だと思いますが）数年前からRC4を優先して使うような設定になっているようです。一方で、デモで使われてしまったpaypal.comではAESが優先されるようになっているようです。</p>
<p>TomcatなどのJavaアプリケーションサーバでは、OracleのJSSE実装を使っている場合、サーバ側でCipherSuiteの優先順位を決めることができません（必ずクライアント側の希望が優先されてしまいます）。この場合の対策については次回のエントリで触れたいと思います。</p>
<p>また、TLS1.1以降ではCBCモードの動作について変更がなされるようなので、その場合にはCipherSuiteの種類によらずBEAST攻撃は不可能となると期待されます。</p>
<p>OpenSSLやGoogle Chromeなどの一部のソフトウェアでは、空のデータを含むSSLレコードを合間に送信することでイヴからコントロールできない形でIV(各ブロックでxorに使われる値)を変化させることで対策を取っています。</p>

<h3>参考にしたサイトなど</h3>

<p>上記2つの論文以外に、次のウェブサイトなどを参考にしました。また、すでにファイルが消えているためURLの掲載はしませんが、BEAST作者による論文(Here comes the ninjas)についても参照しています。</p>
<ul>
<li><a href="http://vnhacker.blogspot.com/2011/09/beast.html" target="_blank">http://vnhacker.blogspot.com/2011/09/beast.html</a></li>

<li><a href="http://isc.sans.edu/diary.html?storyid=7534" target="_blank">http://isc.sans.edu/diary.html?storyid=7534</a></li>
<li><a href="https://blog.torproject.org/blog/tor-and-beast-ssl-attack" target="_blank">https://blog.torproject.org/blog/tor-and-beast-ssl-attack</a></li>
<li><a href="http://www.imperialviolet.org/2011/09/23/chromeandbeast.html" target="_blank">http://www.imperialviolet.org/2011/09/23/chromeandbeast.html</a></li>
<li><a href="http://www.securelist.com/en/blog/208193135/The_SSL_Sky_is_Falling" target="_blank">http://www.securelist.com/en/blog/208193135/The_SSL_Sky_is_Falling</a></li>
<li><a href="http://www.theregister.co.uk/2011/09/19/beast_exploits_paypal_ssl/" target="_blank">http://www.theregister.co.uk/2011/09/19/beast_exploits_paypal_ssl/</a></li>
<li><a href="http://www.theregister.co.uk/2011/09/21/google_chrome_patch_for_beast/" target="_blank">http://www.theregister.co.uk/2011/09/21/google_chrome_patch_for_beast/</a></li>
<li><a href="http://www.theregister.co.uk/2011/09/29/firefox_killing_java/" target="_blank">http://www.theregister.co.uk/2011/09/29/firefox_killing_java/</a></li>
<li><a href="https://src.chromium.org/viewvc/chrome/trunk/src/net/third_party/nss/ssl/ssl3con.c?r1=90632&amp;r2=90643&amp;pathrev=90643" target="_blank">https://src.chromium.org/viewvc/chrome/trunk/src/net/third_party/nss/ssl/ssl3con.c?r1=90632&amp;r2=90643&amp;pathrev=90643</a></li>
</ul>
]]></content>

</entry><entry>
<title>SSL BEASTが利用する「選択平文攻撃」をJavaで実行する方法</title>
<link rel="alternate" type="text/html" href="/information/waf_tech_blog/2011/10/waf-blog-007.html" />
<id>WTB-14_20111014130000</id>
<published>2011-10-14T04:00:00Z</published>
<updated>2011-10-14T04:00:00Z</updated>
<summary>この秋にウェブアプリケーションセキュリティの分野で大きな注目を集めたBEAST(Browser Exploit Against SSL/TLS)の全貌が、ほぼ明らかになってきました。
私たちが開発しているSaaS型WAFサービス、Scutum(スキュータム)はWAFであると同時にHTTPSサーバでもあるため、BEASTが及ぼす影響について非常に気になっており、このたび調査を行いました。本エントリから数回に分けて、BEASTについての情報を中心にお届けします。まず今回は、BEASTの攻撃技術のベースとなっている「CBCモードに対する選択平文攻撃(Chosen Plaintext Attack)」について、実際にJavaのコードを用いて検証してみます。</summary>
<author>
<name>金床　“Kanatoko”</name>
</author>

<content type="html" xml:lang="ja" xml:base="http://www.scutum.jp/information/waf_tech_blog/">
<![CDATA[<h3>はじめに</h3>

<p>この秋にウェブアプリケーションセキュリティの分野で大きな注目を集めた<a href="http://vnhacker.blogspot.com/2011/09/beast.html" target="_blank">BEAST(Browser Exploit Against SSL/TLS)</a>の全貌が、ほぼ明らかになってきました。事前に話題になっていたほどの大きなインパクトを現状のインターネットに及ぼすものではなさそうですが、その中で使われている攻撃テクニックは今後、さらに応用されていく可能性を秘めています。HTTPSに対する攻撃という意味ではひとつの大きなターニングポイントであったと感じます。</p>

<p>私たちが開発している<a href="/" target="_blank">SaaS型WAFサービス「Scutum(スキュータム)」</a>はWAFであると同時にHTTPSサーバでもあるため、BEASTが及ぼす影響について非常に気になっており、このたび調査を行いました。本エントリから数回に分けて、BEASTについての情報を中心にお届けします。まず今回は、BEASTの攻撃技術のベースとなっている「CBCモードに対する選択平文攻撃(Chosen Plaintext Attack)」について、実際にJavaのコードを用いて検証してみます。</p>
]]><![CDATA[<p>なお、BEASTに関連するエントリ中、文章中に登場するアリスとボブは正規のユーザあるいはサーバ管理者を意味し、イヴは攻撃者を意味します。</p>

<h3>選択平文攻撃とは</h3>

<p>「選択平文攻撃」はChosen Plaintext Attackのことで、「選択された平文を用いた攻撃」を意味しますが、ここで「選択する」のは攻撃者であるイヴです。アリスがボブへ秘密のメッセージを送る際に、イヴはすべての（ブロックについて）暗号文が入手可能であり、さらに任意の平文をアリスが送るメッセージ内に挿入することができる、ということが前提となる攻撃になります。選択平文攻撃では攻撃の目的として秘密鍵そのものの入手が挙げられることがありますが、ことBEASTに関してはそうではなく、アリスが書いた平文を手に入れること（つまり復号）が目的となります。</p>

<p>平文全体は、アリスが書いた部分と、イヴが書いた部分から成り立ちます。このうちアリスが書いた内容は、イヴからはわからない、という状態です。なお、アリスが書いた部分が先頭に位置するのか、あるいは別の場所に位置するのか、等についてはここではひとまず置いておくものとしますが、本エントリ後半で解説するJavaのコードを用いた検証の段階では、アリスは先頭の部分を書いているものとしています。</p>

<p>暗号化された秘密の手紙が郵便などの手段で送られる様子をイメージする場合、イヴが、アリスが送るメッセージ内に任意の平文を紛れ込ませる、というケースは通常考えられません。しかしHTTPSのようにコンピュータネットワーク上で行われる暗号通信になると、この攻撃が現実味を帯びてきます。</p>

<h3>CBCモードに対する選択平文攻撃とは</h3>

<p>共通鍵を使ったブロック暗号では、同じ平文がいつも同じ暗号文になってしまわないようにするため、CBCモードが使われることが一般的です。最初にIV(Initialization Vector)と呼ばれるランダムなバイト列を生成し、暗号化する前に平文とxor処理を行います。2ブロック目では、1ブロック目の暗号文をIV代わりに使い、平文とxorしてから暗号化します。これを繰り返すことで、たとえ同じ平文が繰り返し登場するような場合でも、暗号文からはそれが見抜けないことになります。</p>

<p>BEAST以前にSSLに対する選択平文攻撃を指摘していた<a href="http://www.openssl.org/%7Ebodo/tls-cbc.txt" target="_blank">こちらの記事</a>のASCIIアートがわかりやすいので、お借りして掲載します。</p>

<pre>      IV      P1      P2     P3

      |       |       |       |
      |       v       v       v
      |    &gt; XOR   &gt; XOR   &gt; XOR
      |   /   |   /   |   /   |
      |  /    v  /    v  /    v
      | /     E /     E /     E
      |/      |/      |/      |
      v       v       v       v

      IV      C1      C2      C3
</pre>

<ul>
<li>IV:　Initialization Vector</li>
<li>Pn:　平文(nは何ブロック目かを表す数字)</li>
<li>Cn:　暗号文</li>
<li>E:　暗号化処理</li>
</ul>

<p>ここで、選択平文攻撃が実施される例として、以下のような場合を考えます。</p>

<ul>
<li>P1の内容はアリスが決める（ボブに送りたい、秘密のメッセージ）</li>
<li>P2以降の内容はイヴがコントロールできる</li>
<li>イヴの目的はP1を知ること</li>
<li>イヴはメッセージ全体の長さをコントロールできる（好きなだけ、平文に任意のデータを追加できる</li>
</ul>

<p>IVとP1をxorしたものを暗号化するとC1になるので、次の式が成り立ちます。</p>
<pre>E( IV xor P1 ) = C1
</pre>

<p>また、C1は2ブロック目でIV代わりに使用されるので、次の式が成り立ちます。</p>
<pre>E( C1 xor P2 ) = C2
</pre>

<p>iブロック目について考えると、次の式が成り立ちます。</p>
<pre>E( Ci-1 xor Pi ) = Ci
</pre>

<p>P1を得るために、イヴは単純にP1を予想して、片っ端から試していきます。前述したようにIVが使用されるCBCモードであるため、ブルートフォース攻撃が成功しイヴがP1と同じ内容の平文を暗号化しても、暗号化された結果はC1とは等しくなりません。そのため、イヴはブルートフォース攻撃に成功したことに気づくことができないはずです。</p>
<p>しかし、暗号文を入手可能であるイヴは平文のXORで使用される値がCi-1であることがわかっているため、CBCモードであることの（アリスとボブにとっての）利点を打ち消すことができます。これは次のように行われます。</p>
<p>イヴは、自分自身がブルートフォース攻撃に成功したかどうかを知るためには、暗号化された（i番目のブロックの)Ciの値が、C1の値とまったく同じとなるかどうかを調べるしかありません。仮に予想に成功した場合には、次の式が成り立ちます。</p>

<pre>Ci = C1
</pre>
<p>前述した式の内容より、このときは次が成立することになります。</p>
<pre>E( Ci-1 xor Pi ) = C1
</pre>
<p>さらにC1を展開すると次のようになります。</p>
<pre>E( Ci-1 xor Pi ) = E( IV xor P1 )
</pre>
<p>暗号化処理であるEは両辺で共通であるため、これを打ち消します。</p>
<pre>Ci-1 xor Pi = IV xor P1
</pre>
<p>両辺にIVでxorを行います。</p>
<pre>IV xor Ci-1 xor Pi = IV xor IV xor P1
</pre>
<p>IV同士のxorを打ち消すことができます。</p>

<pre>IV xor Ci-1 xor Pi = P1
</pre>
<p>最終的に、左右を入れ替えて次のようになります。</p>
<pre>P1 = IV xor Ci-1 xor Pi
</pre>

<p>このときイヴはIVとCi-1、Piという右辺の値をすべて入手しているので、xorの計算を行うことにより、目的であったP1の内容を手に入れることができます。これがCBCモードに対する選択平文攻撃です。</p>

<p>特徴的な点として、以下が挙げられます。</p>
<ul>
<li>ブルートフォース攻撃の試行回数は暗号化に用いられているアルゴリズム、鍵長と関係がない（ブロックサイズについては影響を受ける場合があります）</li>
<li>攻撃に成功しても秘密鍵を得ることはできない</li>
</ul>

<p>実際には上の例のように、P1のブロックの内容をまるごとブルートフォースで推測するのは時間がかかりすぎる（メッセージ全体のサイズが大きくなりすぎる）ため現実的ではありません。そのため、ブロック中の一部のデータのみが不明である（アリスが決定している）というケースに対する攻撃が現実的となります。</p>

<h3>Javaのコードを用いた検証</h3>

<p>前項のような暗号に関する内容は、文章だけ読んでいると難しく感じるかもしれませんが、コードを書いてみると一目瞭然となります。CBCモードに対する選択平文攻撃は暗号のアルゴリズムに関わらず攻撃可能であるため、ここではBEASTが攻撃したAESとは異なるものとして、Blowfishを選択してみました。AESでは1ブロックのサイズが16バイトであるのに対し、Blowfishでは半分の長さの8バイトとなります。</p>

<p>ここでは非常に単純化したケースを想定して検証を行います。アリスはメッセージの最初の1バイト目にデータを配置します。この1バイト目が何であるかはイヴにはわからず、これを知ることが攻撃の目的となります。2バイト目以降はすべてイヴのコントロール下となり、イヴは好きなだけ、好きな値をここに配置することができるものとします。これは非常に極端な例にも見えますが、実際にはBEASTが行う攻撃はこれに非常に似た形となります。</p>

<p>検証に用いたコードは<a href="http://www.jumperz.net/tools/cbc_attack_example.jar">こちら</a>よりダウンロード可能です（ライセンスはGPLです）。次のように実行することができます。</p>

<pre>java -jar cbc_attack_example.jar
</pre>
<p>実行すると、Blowfishで暗号化された通信に対して選択平文攻撃が実行され、次のような出力となります。IVはランダムに生成されるため、出力の詳細は毎回異なります。</p>
<pre>IV:ccdff23f7b826d4f
C1:cd726e91e1f90f93
--Brute force attack started--
C0 : 6b761f9f5fa73671
C1 : 74be09ca7181473b
C2 : 653316d29be790f6
C3 : 0520ddd7a8358793
C4 : 46ba98768ef6ac6e
(中略)
C64 : 7fef52caaafda9bb
C65 : b7774b6dd7d33104
C66 : a22a37c54fa84425
C67 : 65ce8fa069305ba3
C68 : f282fc03b096a4bc
C69 : cd726e91e1f90f93
--FOUND--
The value of the target is 'E'
</pre>

<p>上記出力の中で、2行目のC1の値と、下から3行目のC69の値が一致しており、ブルートフォース攻撃が成功していることがわかります。</p>

<h3>コード内容の解説</h3>
<p>検証コードの中心となるクラスであるMApp1クラスについて、簡単なコードの解説を掲載します。</p>
<pre>String keyStr = "DEADBEEF";
byte[] keyByte = MStringUtil.hexStringToByteArray( keyStr );
key = new SecretKeySpec( keyByte, ALG );
</pre>
<p>暗号化に使われる共通鍵を定義しています。</p>

<pre>encryptCipher = Cipher.getInstance( ALG + "/" + MODE + "/" + PADDING );

in = new PipedInputStream();
pout = new PipedOutputStream( in );

encryptCipher.init( Cipher.ENCRYPT_MODE, key );
OutputStream out = new CipherOutputStream( pout, encryptCipher );
</pre>
<p>暗号化を実行するCipherクラスのインスタンスを生成し、続いて通信経路をシミュレートするためのストリームを定義します。ここではCipherOutputStreamと組み合わせてPipedStreamを使います。ストリームへのデータの書き込みは自動的に暗号化されますが、ストリームからの読み込みは暗号化されたバイト列がそのまま出てきます（復号されていません）。これによって、イヴのネットワーク上での盗聴をシミュレートします。</p>

<pre>SecureRandom random = new SecureRandom();
byte[] ivSeed = new byte[ 8 ];
random.nextBytes( ivSeed );
out.write( ivSeed );
byte[] encIV = read();
System.out.println( "IV:" + MStringUtil.byteToHexString( encIV ) );
</pre>
<p>ランダムなバイト列を生成し、ストリームに書き込みます。そして暗号化されたバイト列をIVとして使用します（通常のCBCモードの通信では、生成したバイト列は暗号化せず、そのままIVとして利用することが多いようです）。read()関数はストリームから読み込み可能なバイト列を取得するユーティリティメソッドで、イヴの盗聴によるデータ取得をシミュレートします。</p>

<pre>String valueOfAlice	= "E";		//1byte
String valueOfEve	= "FFFFFFF";	//7bytes
String p1Str = valueOfAlice + valueOfEve;
</pre>
<p>アリスが送る秘密の文字を定義します。ここでは前述したようにたった1バイトでの検証となります。ここでは例として大文字の「E」を使用していますが、これはE以外にしても問題ありません。イヴの目的はこの「E」という文字を得ることとなります。</p>
<p>続いてイヴは「FFFFFFF」という7バイトのデータをまず最初に送るものとします。これは、7バイトの長さであれば内容はどんなものでも意味的に変わりはありません。7バイトのデータをアリスの1バイトのデータにくっつけることで、ちょうどBlowfishの暗号化ブロックの長さである8バイトとなります。これによって、平文の1ブロック目であるP1の内容は「EFFFFFFF」となります。</p>
<pre>byte[] p1 = p1Str.getBytes();
out.write( p1Str.getBytes() );
out.flush();
byte[] c1 = read();
String c1Str = MStringUtil.byteToHexString( c1 );
System.out.println( "C1:" + c1Str );
</pre>
<p>P1が暗号化されてネットワーク上を通る様子をイメージします。read()によって、イヴが盗聴可能な値である、「P1が暗号化されたデータ」つまりC1のバイト列が得られます。</p>
<pre>byte[] ci_1 = Arrays.copyOf( c1, c1.length );
byte[] p1expected = Arrays.copyOf( p1, p1.length );
System.out.println( "--Brute force attack started--" );
for( int i = 0; i &lt; 256; ++i )
    {
    p1expected[ 0 ] = ( byte )i;
    byte[] bi = MSecurityUtil.xor( encIV, p1expected );
    byte[] pi = MSecurityUtil.xor( bi, ci_1 );
    out.write( pi );
    out.flush();
    byte[] ci = read();
    String ciStr = MStringUtil.byteToHexString( ci );
    System.out.println( "C" + i + " : " + ciStr );
    if( ciStr.equals( c1Str ) )
        {
        System.out.println( "--FOUND--" );
        System.out.println( "The value of the target is '" + new String( p1expected ).substring( 0, 1 ) + "'" );
        break;
        }
    ci_1 = ci;
    }
</pre>
<p>アリスが決めた1バイト目は256通りの可能性があります。ここでは単純に0から順番にブルートフォース攻撃を行います。先述したように下記が成り立ちます。</p>

<pre>P1 = IV xor Ci-1 xor Pi
</pre>
<p>そのため、「P1と予想されるバイト列」であるp1expectedとIVでxor処理を行い、さらにCi-1を意味するCi_1との間でxor処理をしてからストリームに書き込みます。暗号化の結果得られるバイト列が1ブロック目の暗号化の結果であるC1と同じであれば、ブルートフォース攻撃に成功したと判断し、p1expectedの内容はP1と等しいと判断できます。</p>
<p>このように、たった256回の試行で必ずアリスの文字を特定することができます。仮にアリスのメッセージの長さが2バイトであれば、256*256となり、最大で65536回の試行が必要となります。3バイトであればさらに256倍の試行が必要になる可能性があります。つまりCBCモードに対する選択平文攻撃では、イヴが、アリスのメッセージを含むブロックについて、何バイトを制御可能であるか、という点が攻撃の可能性を大きく左右する要素となります。</p>
<p>BEASTが目を付けたのはまさにこの点であるのですが、これについては次回詳しく解説を行いたいと思います。</p>
]]></content>

</entry><entry>
<title>SSL中間CA証明書を検索するウェブサービス、SSLDB.info</title>
<link rel="alternate" type="text/html" href="/information/waf_tech_blog/2011/09/waf-blog-006.html" />
<id>WTB-13_20110926130000</id>
<published>2011-09-26T04:00:00Z</published>
<updated>2011-09-26T04:00:00Z</updated>
<summary>SSLを使うウェブサイトでは、ウェブサーバへのサーバ証明書のインストール作業が必要となります。このときややこしいのが、中間CA証明書の扱いです。多くの場合、証明書会社が発行するサーバ証明書は、ユーザのウェブブラウザにインストールされているルート証明書から直接署名されているわけではありません。ルート証明書によって署名された中間CA証明書によって署名されています。そのため、ブラウザはサーバ証明書とルート証明書の信頼のチェーンを辿るために、中間CA証明書を必要とします。</summary>
<author>
<name>金床　“Kanatoko”</name>
</author>

<content type="html" xml:lang="ja" xml:base="http://www.scutum.jp/information/waf_tech_blog/">
<![CDATA[<h3>はじめに</h3>
<p>SSLを使うウェブサイトでは、ウェブサーバへのサーバ証明書のインストール作業が必要となります。このときややこしいのが、中間CA証明書の扱いです。多くの場合、証明書会社が発行するサーバ証明書は、ユーザのウェブブラウザにインストールされているルート証明書から直接署名されているわけではありません。ルート証明書によって署名された中間CA証明書によって署名されています。そのため、ブラウザはサーバ証明書とルート証明書の信頼のチェーンを辿るために、中間CA証明書を必要とします。</p>

<p>中間CA証明書が見当たらない場合、サーバ証明書が正しいものであるにもかかわらず、ブラウザはエラーと判断してしまいます。そのため、サーバ証明書をインストールする場合には、それに対応する中間CA証明書も一緒にインストールするのが一般的な方法となります。</p>
]]><![CDATA[
<h3>ややこしさを増す中間CA証明書</h3>
<p>最近では推奨されるRSAの鍵長が1024bitから2048bitへと変化したことなどから、各証明書会社において、主に用いられるルート証明書に変化がありました。しかし、古いブラウザからのアクセスも依然として無視できないため、新しい2048bitのルート証明書と、古い1024bitの証明書のどちらからでも信頼のチェーンを辿ることができるように、クロスルート方式とよばれる中間CA証明書のインストール方法がメジャーなものとなってきています。</p>

<p>この場合、具体的には中間CA証明書を2枚インストールすることが多くなります。また、動作確認のためには新しいブラウザと古いブラウザの両方でアクセスを行い、問題がないかどうか試す必要があります。そのため「サーバ証明書をウェブサーバにインストールする際に、どの中間CA証明書を一緒にインストールするべきか？」という問題はややこしさを増しており、ウェブサーバ管理者の悩みの種となっていると言えるでしょう。</p>


<h3>Scutumにおける中間CA証明書管理の問題</h3>
<p><a href="/" target="_blank">SaaS型のWAFサービスであるScutum</a>（スキュータム）では、ScutumはSSLのサーバとしても動作します。そのためサービスの運営においてサーバ証明書や中間CA証明書、秘密鍵などを多く扱います。</p>

<p>現在のサービスではお客様が専用のウェブ管理画面からサーバ証明書や中間CA証明書などの必要なファイルをアップロードし、その後Scutumの担当エンジニアが内容を確認してからScutumへのサーバ証明書のインストール作業を行う、という形となっています。つまり、人手を用いての作業となっています。</p>

<p>これまでにサービスを運営してきた中で我々が気がついたのは、非常に多くのお客様において、中間CA証明書が正しく扱われていないということです。必要とされる中間CA証明書を正しくアップロードされるお客様は割合として約半数といったところです。残りの半数のお客様は以下のような間違いをされていることが多くなっています。</p>

<ul>
<li>中間CA証明書をまったくアップロードされない</li>
<li>誤った内容の中間CA証明書をアップロードされる</li>
<li>アップロードされる中間CA証明書が足りない（前述したクロスルート方式に対応できない）</li>
</ul>

<p>このような状況の中で、我々Scutumのエンジニアは、お客様の証明書の情報を元に正しい中間CA証明書を見つけ、間違っている場合には正しいものを用いてインストール作業を行う、といった運用を行ってきました。しかしこの作業はヒューマンスキルに依存する部分が多く、ミスをしやすい要素が多かったため、サービス運用チームの中ではかねてより自動化することが課題のひとつとなっていました。</p>

<h3>独立したウェブサービスとして開発</h3>

<p>今年（2011年）になって、下記の確証が得られるようになってきました。</p>

<ul>
<li>サーバ証明書から中間CA証明書を自動的に見つける（検索する）機能を実現すれば、省力化と作業ミスの低減に繋げられる</li>
<li>サーバ証明書をもとにした中間CA証明書の検索機能は、実用的な精度で実装が可能である</li>
</ul>


<p>そこで、2011年の8月初旬から「中間CA証明書検索機能」の実装を開始しました。開発にはJavaとMongoDBを用い、以下のような項目に沿って進めました。</p>

<ul>
<li>広く使われているメジャーなルート証明書を元にする</li>
<li>現在インターネット上で使用されている中間CA証明書のデータを多く集める</li>
<li>これらによって形成される信頼のチェーンのパターンを蓄積する</li>
<li>蓄積されたパターンを検索する機能を作る</li>
</ul>


<p>途中、予想していたよりもSSLの海が深かったために何度も溺れそうになりましたが、9月の中旬になんとか実用的なレベルまで仕上げることができました。この検索機能は単体で完結するシンプルなものであり、またオープンソースソフトウェアと同様に、コミュニティによるフィードバックが得られればより良いクオリティに繋がると判断しました。そこで、Scutumとは切り離し、独立したウェブサービスとして、インターネット上で無料で公開することとしました。</p>

<p><b>SSLDB.info</b><br />
<a href="http://www.ssldb.info/" target="_blank">http://www.ssldb.info/</a>
<img src="http://www.ssldb.info/images/key.png" height="64px" width="64px" />
</p>

<p>PEM形式のサーバ証明書を貼り付け、「search」ボタンを押すと、一緒にインストールすべき中間CA証明書がPEM形式で得られるようになっています。検索結果は単純に信頼のチェーンの条件を満たすものではなく、実際にインターネット上で利用されている頻度や証明書の有効期限などから総合的に判断されたものが出てくるようになっています。このチューニングはサービスの品質に直結しており、今後もより高い精度を目指してテストとチューニングを重ねていきたいと考えています。</p>

<p>今後はScutumやAWS(Amazon Web Services)のELBのように、SSL証明書を利用者側が管理していくサービスが増えていくものと思われますが、そのようなサービスを設計している人からも利用できるよう、今後APIなども提供できればと考えています。</p>

<h3>バックグラウンド</h3>
<p>データベースの構築については、ユタ州立大学(USU)のネットワーク管理者の方から教えて頂いた、<a href="https://www.eff.org/observatory" target="_blank">EFFのSSL Observatory Project</a>のデータを出発点にしました。彼らが集めた100万単位のSSL証明書のデータがEC2上やBitTorrentから無料でダウンロードできるようになっています。私はBitTorrentでダウンロードしましたが、なんとサイズが16GBもあるMySQLのダンプファイルで、リストアするのに一晩かかりました。</p>

<p>現在サーバはAWSの東京リージョンを利用しており、microインスタンスで動作しています。MongoDB＋Tomcatという非常にシンプルな作りで、microインスタンスでも十分性能が出ているようです。また、DNSには使いやすいインターフェースによる手軽なDNS管理を実現した<a href="https://dozens.jp/" target="_blank">Dozens.jp</a>を利用しています。</p>

<p>SSLDB.infoの<a href="http://twitter.com/ssldb/" target="_blank">twitterアカウント</a>も用意しましたので、フィードバック送っていただければありがたいです（もちろん日本語でOKです）。ウェブサーバ管理者の一手間を省くことのできるサービスとしてご利用いただければ幸いです。</p>

]]></content>

</entry><entry>
<title>Amazon EC2のAvailability Zoneの名前はアカウント毎に違う</title>
<link rel="alternate" type="text/html" href="/information/waf_tech_blog/2011/08/waf-blog-005.html" />
<id>WTB-12_20110829140000</id>
<published>2011-08-29T05:00:00Z</published>
<updated>2011-08-29T05:00:00Z</updated>
<summary>先のエントリにて、EC2のデータ転送と課金の関係についてまとめました。EC2では、同一のAZ内に存在しているインスタンス間でのデータ転送は、プライベートIPを使ったものについては課金されません。
Scutum(スキュータム)は SaaS型のWAFなので、お客様のウェブサーバとScutumのWAFインスタンスの間にそれなりの量のトラフィックが発生します。そのため、この課金されないトラフィックをうまく使いたいと考えていますが、ここに落とし穴が潜んでいました。実は、EC2でのAZは、アカウント毎に指している実体が異なることがあるのです。</summary>
<author>
<name>金床　“Kanatoko”</name>
</author>

<content type="html" xml:lang="ja" xml:base="http://www.scutum.jp/information/waf_tech_blog/">
<![CDATA[
<h3>はじめに</h3>

<p><a href="/information/waf_tech_blog/2011/08/waf-blog-004.html">先のエントリ</a>にて、EC2のデータ転送と課金の関係についてまとめました。EC2では、同一のAZ内に存在しているインスタンス間でのデータ転送は、プライベートIPを使ったものについては課金されません。</p>
<p><a href="http://www.scutum.jp/" target="_blank">Scutum(スキュータム)</a>はSaaS型のWAFなので、お客様のウェブサーバとScutumのWAFインスタンスの間にそれなりの量のトラフィックが発生します。そのため、この課金されないトラフィックをうまく使いたいと考えていますが、ここに落とし穴が潜んでいました。実は、EC2でのAZは、アカウント毎に指している実体が異なることがあるのです。</p>

 <p><a href="/information/waf_tech_blog/images/ec2_az.png" border="0" target="_blank"><img src="http://www.scutum.jp/information/waf_tech_blog/images/ec2_az.png" width="300" height="278" /></a><br />
(クリックで拡大します)</p>

]]><![CDATA[<h3>発見に至ったきっかけ</h3>
<p><a href="/information/waf_tech_blog/2011/08/waf-blog-004.html">先のエントリ</a>の内容を調査する段階で、私たちは2つの異なるAWSアカウントを用意しました。これは(ScutumがEC2でサービスを提供する場合には)、お客様のウェブサーバのインスタンスの管理アカウントと、ScutumのWAFインスタンスの管理アカウントが異なるためです。前者はもちろんお客様のAWSアカウント、後者は私たちScutum管理者が利用するAWSアカウントとなります。</p>
<p>異なるアカウントに所属するインスタンス間のデータ転送に対する課金を調べてみたところ、なぜかRegional Data Transferが課金されないという現象に遭遇しました。ap-northeast-1aのインスタンスとap-northeast-1bのインスタンスの間で5GBや10GBといった量のデータを転送したにもかかわらず、数日待っても課金が行われないのです。そこで、AWSのサポートフォーラムで質問してみました。該当のスレッドは<a href="https://forums.aws.amazon.com/thread.jspa?messageID=273850" target=_blank>こちら</a>になります。</p>
<p></p>

<h3>AZの名前は単なるマッピングである</h3>
<p>AWS側からはすぐに返答を頂くことができました（それも日本語で。感謝）。それによると、AZの名前はアカウント毎に異なる場合があるとのこと。理由は書かれていませんが、おそらくユーザが無意識に最初のAZを選択してしまうことなどによって、AZ間のサーバの数が偏ってしまうのを避けるための仕組みではないかと思います。</p>
<p>私たちが立ち上げた2つのインスタンスは、それぞれ「ap-northeast-1a」と「ap-northeast-1b」で起動していたのですが、実際には同一のAZだったのです。そのため、データ転送はAvailability Zone Data Transferとして扱われ、課金が行われなかったのです。</p>
<p>このようなアカウント毎のマッピングの違いは東京リージョンに限らずEC2の仕様のようです（オフィシャルのドキュメントは見つけられていませんが、AWSのスタッフの回答にあるため、間違いないでしょう）。また、調べてみたところ、2つの関連エントリを見つけることができました(<a href="http://alestic.com/2009/07/ec2-availability-zones" target=_blank>こちら</a>と<a href="http://d.hatena.ne.jp/rx7/20100721/p1" target=_blank>こちら</a>)。</p>
<p>障害が発生した際に「ap-northeast-1aで障害が発生しているようだ」などとtwitterでつぶやいても、実は混乱をまねいてしまう可能性があるようです。</p>
<p>

<h3>2つのアカウント間でのズレを調べる方法(IPアドレスを使う)</h3>

<p>このようにややこしい背景があるため、2つのアカウント間でAZの認識がずれているかどうか調べたいケースがあるかと思います。おそらく最も簡単なのは、2つのインスタンス間で、プライベートIPを使ったtracerouteをしてみることです。</p>
<pre>
[root@ec2-1 ~]# traceroute -n -T 10.150.105.214
traceroute to 10.150.105.214 (10.150.105.214), 30 hops max, 60 byte packets
 1  10.146.8.3  12.042 ms  12.022 ms  12.010 ms
 2  175.41.192.174  1.172 ms  1.176 ms  1.165 ms
 3  175.41.192.11  3.238 ms  3.408 ms  3.444 ms
 4  175.41.192.221  9.537 ms 175.41.192.225  3.325 ms 175.41.192.221  9.511 ms
 5  10.150.105.214  3.318 ms  3.307 ms  3.301 ms
</pre>

<p>このように5hopほどある場合には、異なるAZ間に所属している可能性が高いです。同一のAZにある場合には、2hopくらいになるようです(ただし、東京リージョンでしかテストしていません）。</p>
<p>また、実はIPアドレスの先頭の、10.146と10.150という部分だけに注目してしまってもよいのかもしれません。</p>
<p>これらは手軽ですが、100%確実な方法ではありません。</p>

<h3>2つのアカウント間でのズレを調べる方法(Spot Instanceの相場を使う)</h3>
<p>ブラウザでAWSのEC2コンソールにアクセスし、「Spot Requests」から「Pricing History」を表示します。すると過去のSpot Instances価格のグラフが表示されます。このグラフはAZ別に色分けされた線によって描かれるため、もし2つのアカウント間で色が逆転していれば、その2つのアカウントではAZの名前のマッピングが逆になっていることがすぐにわかります。私たちが利用した2つのアカウントでは以下のようになっていました。</p>

<a href="../../images/az_1.png" border="0" target=_blank><img src="../../images/az_1.png" width="400" height="180" /></a><br />
<a href="../../images/az_2.png" border="0" target=_blank><img src="../../images/az_2.png" width="400" height="180" /></a><br />
<p>グラフに目立ったピークがなくわかりにくい場合には、Date Rangeを長めにするのがよいのではと思います。この方法を使えば、確実にマッピングのズレを把握することができます。また、インスタンスを起動することなく実施することができる方法になります。</a>

<h3>まとめ</h3>
<p>今回はEC2のAZにおいて、アカウントによってマッピングが異なっている場合とその判別方法についてまとめました。文中からリンクしているエントリ内では、ec2コマンドを使って調べる方法も紹介されていますので、興味がある方は調べてみてください。</p>
<p>また、本エントリについてのフィードバックはお気軽に@kinyukaまでいただければ幸いです。</p>
]]></content>

</entry><entry>
<title>Amazon EC2のデータ転送量と課金まとめ</title>
<link rel="alternate" type="text/html" href="/information/waf_tech_blog/2011/08/waf-blog-004.html" />
<id>WTB-11_20110829130000</id>
<published>2011-08-29T04:00:00Z</published>
<updated>2011-08-29T04:00:00Z</updated>
<summary>クラウドサービスの代名詞でもあるAWS(Amazon Web Services)が待望の東京リージョンを開設してしばらく経ちました。我々Scutum(スキュータム)はSaaS型のWAF(Web Application Firewall)サービスであるため、AWSのEC2のようなIaaS型のインフラ上に積極的に展開をおこなっています。本エントリではその際にポイントとなる、EC2でのデータ転送量への課金について調べてみました。</summary>
<author>
<name>金床　“Kanatoko”</name>
</author>

<content type="html" xml:lang="ja" xml:base="http://www.scutum.jp/information/waf_tech_blog/">
<![CDATA[<h3>はじめに</h3>

<p>クラウドサービスの代名詞でもあるAWS(Amazon Web Services)が待望の東京リージョンを開設してしばらく経ちました。我々<a href="http://www.scutum.jp/" target="_blank">Scutum(スキュータム)</a>はSaaS型のWAF(Web Application Firewall)サービスであるため、AWSのEC2のようなIaaS型のインフラ上に積極的に展開をおこなっています。本エントリではその際にポイントとなる、EC2でのデータ転送量への課金について調べてみました。</p>

 <p><a href="/information/waf_tech_blog/images/ec2_transfer.png" border="0" target="_blank"><img src="http://www.scutum.jp/information/waf_tech_blog/images/ec2_transfer.png" width="400" height="371" /></a><br />
(クリックで拡大します)</p>

<p>このエントリの内容は<a href="http://aws.amazon.com/ec2/pricing/" target="_blank">オフィシャルのウェブページ</a>やインターネット上の情報を元にしたものであり、実情と異なっている可能性もあるのでご注意ください。また、価格は東京リージョンのものとなっています。</p>
]]><![CDATA[

<h3>AWSの複雑な課金体系</h3>
<p>AWSの特徴のひとつとして、課金の仕組みが非常に複雑であることが挙げられます。これは高いオンデマンド性を実現するために仕方ない面があるかと思いますが、国内の競合IaaSであるIIJGIOやNiftyクラウド、（IaaSではありませんが）さくらのVPSがいずれも非常にシンプルな課金体系を実現していることに対しては、やや不利な面と言えるでしょう。</p>

<p>データ転送量に対する課金だけを見た場合でも、EC2では4種類もの分類があります。また、そのうちのひとつである「Internet Data Transfer」について見てみると、「最初の1GB」「1GBから10TBまで」「10TBから40TBまで」「...」のように、総転送量によって単位あたりの料金が異なっており、結局どの程度の転送量でいくらくらいになるのかをイメージすることが難しい状態となっています。Amazon側もこのような認識がある（？）ようで、ブラウザ上で課金をシミュレートして確認できる<a href="http://aws.amazon.com/calculator" target="_blank">公式の計算機</a>が利用できるようになっていますが、データ転送についてはあまり細かいシミュレーションはできないようです。</p>

<h3>4種類のデータ転送</h3>
<p>それでは具体的にデータ転送量について見ていきます。先述したようにEC2でのデータ転送には4つの分類があります。以下、それぞれの項目について解説します。</p>
<p>(なお、データ転送についての課金では、異なるAWSアカウントに所属するインスタンス同士の通信でも同一AWSアカウントに所属するインスタンス同士の場合と同じように扱われます。)</p>

<h4>Internet Data Transfer</h4>
<p>図の赤い線を指します。EC2からインターネット上のマシンに送られるデータや、EC2のリージョンをまたいで（東京からカリフォルニアへ、など）送られるデータがこれにあたります。当然ながら4種類のうちでもっとも高価で、1GBあたり$0.201、日本円で約15円となっています。先日行われた値下げにより、EC2側から見てINのデータ（インターネット側からEC2側に送られてくるデータ）は無料となっているため、課金対象となるのはEC2側から見てOUTのデータのみとなります。</p>

<h4>Availability Zone Data Transfer</h4>
<p>図の青い線を指します。同一のAZにあるインスタンスの間で、プライベートIPアドレスを使って通信する場合の分類です。これは無料となっています。</a>

<h4>Regional Data Transfer</h4>
<p>図の緑の線を指します。同一リージョン(例：東京)にある、異なるAZ間(例：ap-northeast-1aとap-northeast-1b）において、プライベートIPアドレスを使って通信した場合の分類です。こちらは残念ながら課金対象となります。しかし料金は非常に安めで、1GBあたり$0.01となっています。Internet Data Transferに比べて約1/20の料金です。IN側とOUT側の両方に課金が生じます。</p>

<h4>Public and Elastic IP and Elastic Load Balancing Data Transfer</h4>
同一リージョン内での通信にグローバルIPアドレス(インスタンスのPublicなIPアドレス、Elastic IP、ELB)を使った場合の分類になります。この場合、通信する2つのインスタンスがそれぞれどのAZにあっても関係ありません。価格はRegional Data Transferと同じで1GBあたり$0.01となります。IN側とOUT側の両方に課金が生じます。

<h3>まとめ</h3>
<p>このように、EC2では4種類のデータ転送が存在しています。基本的にはインターネット側へ出て行くデータの量だけを気にしていればよさそうですが、インスタンス間で大量のデータを移動させる場合などは、同一AZ間でプライベートIPアドレスを使った通信を行うと無料であることを覚えておくとよいでしょう。</p>
<p>また、別のAWSアカウントに所属するインスタンス同士で、課金を気にしながら大量のデータ転送を行う場合には注意が必要です。AZの実体はアカウントごとに異なる場合があるため、同一AZだと思っていても、実際には異なる可能性があります。この点については近日公開予定の次のエントリを参照ください。</p>
]]></content>

</entry><entry>
<title>PHP環境で発生するMongoDB Request Injection攻撃</title>
<link rel="alternate" type="text/html" href="/information/waf_tech_blog/2011/08/waf-blog-003.html" />
<id>WTB-10_20110803130000</id>
<published>2011-08-03T04:00:00Z</published>
<updated>2011-08-03T04:00:00Z</updated>
<summary>WAF「Scutum（スキュータム）」では、最近バックエンドのDBにMongoDBの導入を進めています。私も個人的にPHPのフレームワークであるCakePHP向けにMongoDBを扱うプラグインCakePHP-MongoDB Datasourceを開発し、公開しています。
今回は、PHPからMongoDBを扱う際の注意点として、RequestInjection攻撃に関して書きたいと思います。この問題は、phpマニュアルのMongoドライバーの章でも扱われています。この記事では、はじめにPHPでMongoを操作する方法、PHPの基本を書き、最後にInjection攻撃の仕組みと対応方法を書きます。</summary>
<author>
<name>市川　（@cakephper）</name>
</author>

<content type="html" xml:lang="ja" xml:base="http://www.scutum.jp/information/waf_tech_blog/">
<![CDATA[<h3>はじめに</h3>

<p>SaaS型<a href="http://www.scutum.jp/index.html" target="_blank">WAF「Scutum（スキュータム）」</a>では、最近バックエンドのDBにMongoDBの導入を進めています。私も個人的にPHPのフレームワークであるCakePHP向けにMongoDBを扱うプラグインCakePHP-MongoDB Datasourceを開発し、公開しています。<br /> <a href="https://github.com/ichikaway/cakephp-mongodb/" target="_blank">https://github.com/ichikaway/cakephp-mongodb/</a></p>

<p>今回は、PHPからMongoDBを扱う際の注意点として、RequestInjection攻撃に関して書きたいと思います。この問題は、<a href="http://www.php.net/manual/en/mongo.manual.php#mongo.security" target="_blank">phpマニュアルのMongoドライバーの章</a>でも扱われています。<br />この記事では、はじめにPHPでMongoを操作する方法、PHPの基本を書き、最後にInjection攻撃の仕組みと対応方法を書きます。</p>]]><![CDATA[<h3>PHPでMongoDBを扱うには</h3>

<p>PHPでMongoDBを操作する場合、10gen社が開発している<a href="http://pecl.php.net/package/mongo" target="_blank">pecl mongoドライバー</a>を利用します。データをusersコレクション(Table)から取得する場合は、下記のようにします。</p>

<pre>
$mongo = new Mongo('mongodb://localhost:27017');
$collection = $mongo-&gt;selectDB("foo")-&gt;selectCollection("users");
$cursor = $collection-&gt;find(array('status' =&gt; array('$gte' =&gt; 1)));
</pre>

<p>findの検索条件は、連想配列で記述します。MongoDBのオペレータは配列のKeyに文字列として '$gt' というように記述します。ここでは、usersコレクションのstatusカラムに、1以上がセットされているユーザを取得する条件がセットされています。</p>

<h3>PHPの基本</h3>

<p>PHPではGet/Postリクエストで送信したデータが$_GET/$_POSTというグローバル変数に配列として格納されます。例えば、<br />index.php?foo=bar というURLにアクセスすると、$_GET['foo']にbarという文字列が入ります。<br />さらに、クエリストリングのKeyを配列のように記述すると、サーバ側では配列として格納されます。<br />例えば、 index.php?foo[foo2][foo3]=bar<br />というURLにアクセスすると、$_GET['foo']['foo2']['foo3']にbarという文字列が入ります。</p>

<h3>PHPへのMongoDB Request Injection攻撃</h3>

<p>ここで説明するInjection攻撃は、外部からMongoDBオペレータを注入できるというものです。例えば、usersコレクションから該当ユーザをID/PWで引き当てる場合、</p>

<pre>
$cursor = $collection-&gt;find(array(
   "username" =&gt; $_GET['username'],
   "passwd" =&gt; $_GET['passwd']
));
</pre>

<p>というような処理があったとしましょう。<br />そのphpスクリプトに対して、下記のようなリクエストを送ると、adminユーザが引き当てられてしまいます。</p>

<pre>
login.php?username=admin&amp;passwd[$ne]=1
</pre>

<p>これは「PHPの基本」の章で書いたように、PHPはKeyの記述によってそのまま配列に展開する機能があるためです。実際のプログラムでは下記のように実行されてしまいます。</p>

<pre>
$cursor = $collection-&gt;find(array(
   "username" =&gt; "admin",
   "passwd" =&gt; array("$ne" =&gt; 1)
));
</pre>

<p>PHPが送信データのKeyの文字列を解析して配列に展開してしまうことと、MongoDBのオペレータが$gtのような文字列で指定できることが組み合わさり、外部からMongoDBオペレータを注入できるというのが、この問題の原因です。</p>

<p>※今回はGETで説明していますが、POSTも同様の問題があります。</p>

<h3>対応方法</h3>

<p>この問題を回避するために、文字列で値を指定する箇所は下記のようにします。配列で値が渡ってきたとしても、username/passwdの値はArrayという文字列になるため問題を回避できます。</p>

<pre>
 $cursor = $collection-&gt;find(array(
   "username" =&gt; (string)$_GET['username'],
   "passwd" =&gt;  (string)$_GET['passwd']
 ));
</pre>

<p>上記の対応方法は、アプリケーションを実装する上で常に注意する必要があります。面倒な場合は$_POST/$_GETなどのユーザ入力値を全てチェックして、Keyに不正な文字(ここでは$文字)が含まれている場合は、エラーやunset()で値を消すという方法もあります。この方法は毎回ユーザ入力データをスキャンするため多少負荷がかかりますのでご注意ください。</p>

<p>この方法を手軽に実現するために、私が開発している下記のツールがあります。<br /><a href="https://github.com/ichikaway/SecurePHP" target="_blank">https://github.com/ichikaway/SecurePHP</a></p>

<p>このツールをダウンロードし、下記のようにbootstrap.phpをアプリケーションの最初に読み込むと、Keyの文字チェック、値のnullバイトチェックなどを行います。</p>

<pre>
require_once(YOUR_LIB_PATH . 'SecurePHP/config/bootstrap.php');
</pre>

<p>配列のKeyは、英数字と:_./-のみを許すようにしていますので、$が含まれていると値がunsetされます。これは$allowKeyNameRegexというクラス変数で定義しているので、アプリケーションにフィットしなければ変更してご利用ください。</p>

<h3>結論</h3>

<p>PHPでMongoDBを扱うのはPeclMongoドライバーのおかげで非常に簡単です。ただし、このような言語の特性によって起こる問題もあるため、常に最新のPHPマニュアルを参照すると良いと思います。<a href="http://www.php.net/manual/en/book.mongo.php" target="_blank">英語ドキュメント</a>が一番更新が早いため、そちらをチェックすることをお勧めします。</p>

<h3>参考資料</h3>

<p>「Do I Have to Worry About SQL Injection」<br /><a href="http://www.mongodb.org/display/DOCS/Do+I+Have+to+Worry+About+SQL+Injection" target="_blank">http://www.mongodb.org/display/DOCS/Do+I+Have+to+Worry+About+SQL+Injection</a></p>
<p>「PHPマニュアル」<br /><a href="http://www.php.net/manual/en/mongo.manual.php#mongo.security" target="_blank">http://www.php.net/manual/en/mongo.manual.php#mongo.security</a></p>
<p>「Mongodb is vulnerable to SQL injection in PHP at least」<br /><a href="http://www.idontplaydarts.com/2010/07/mongodb-is-vulnerable-to-sql-injection-in-php-at-least/" target="_blank">http://www.idontplaydarts.com/2010/07/mongodb-is-vulnerable-to-sql-injection-in-php-at-least/</a></p>
]]></content>

</entry><entry>
<title>Javaプログラマから見たMongoDB</title>
<link rel="alternate" type="text/html" href="/information/waf_tech_blog/2011/07/waf-blog-002.html" />
<id>WTB-9_20110715130000</id>
<published>2011-07-15T04:00:00Z</published>
<updated>2011-07-15T04:00:00Z</updated>
<summary>私は6月から7月にかけて、JavaからMongoDBにアクセスするコードを初めて書いてみました。今までSQLと、JDBCドライバのAPIを直接呼び出すJavaプログラミングに慣れていたため、非常に新鮮に感じる要素が多々ありました。まだMongoDBに慣れきっておらず、新鮮に感じる視点が残っているうちに、JavaからMongoDBにアクセスする場合にプログラマの立場から受けた印象や気になった点を中心に本エントリにまとめてみたいと思います。</summary>
<author>
<name>金床　“Kanatoko”</name>
</author>

<content type="html" xml:lang="ja" xml:base="http://www.scutum.jp/information/waf_tech_blog/">
<![CDATA[<img src="/information/waf_tech_blog/images/sun-java-logo.jpg" height="90px" />

<h3>はじめに</h3>
<p>PostgreSQLなどのRDBMSでは、コマンドラインのクライアントからSQL文を打ち込んでデータを操作することが一般的です。これに対し、MongoDBでは、コマンドラインのクライアント（mongoコマンド）からJavaScriptを使ってデータにアクセスすることができます。ただし、JavaScriptを使うといってもif文やfor文などを使うことはまれで、多くの場合は単純にJSON形式で記述したクエリーを関数の引数にして実行するだけになります。その際「find」「update」などの関数や「$gt」「$set」などのMongoDB独自のオペレータを用います。これは特定のプログラミング言語に特化した方法ではなく、少し抽象化された、MongoDB言語とでも呼べるものとなっています。</p>
]]><![CDATA[<p>PHPやJavaなど、JavaScript以外で書かれたアプリケーションからMongoDBにアクセスする際も基本は同じです。mongoクライアントでアクセスする際のJSON形式のクエリーと同じように、それぞれのプログラミング言語の中で階層化されたデータ構造（Map、連想配列などと呼ばれるもの）を使ってクエリーを作成し、関数を実行することになります。そのため、プログラマはまずmongoクライアントでMongoDBの基本的な操作に慣れた後に、自分の使う言語でそれをどのように表現するのかをマッピングしていくことになります。</p>

<p>私は6月から7月にかけて、JavaからMongoDBにアクセスするコードを初めて書いてみました。今までSQLと、JDBCドライバのAPIを直接呼び出すJavaプログラミングに慣れていたため、非常に新鮮に感じる要素が多々ありました。まだMongoDBに慣れきっておらず、新鮮に感じる視点が残っているうちに、JavaからMongoDBにアクセスする場合にプログラマの立場から受けた印象や気になった点を中心に本エントリにまとめてみたいと思います。</p>

<h3>ドライバのソースコードをEclipse上にインポートしておくと便利</h3>
<p>JavaでMongoDBにアクセスするには専用のドライバを使います。これはMongoDBを開発している10gen社から正式にリリースされており、githubでソースコードも手に入れることができます（ライセンスはApache License Version 2.0です）。私はかなり早い段階でこのドライバのソースコードを自分の開発環境のEclipseに取り込み、開発中のアプリケーションからドライバ内のソースコードへ自由にジャンプできるようにしました。これが後々とても役に立ったので、Javaで開発される方には非常におすすめできる方法です。</p>

<h3>ユーティリティクラスやメソッドを作ると便利</h3>
<p><a href="http://www.mongodb.org/display/DOCS/Java+Tutorial">こちら</a>にあるJavaチュートリアルの中で、「Inserting a Document」という項目に、下記のようなJSONとJavaコードのマッピングの例があります。</p>

JSON形式
<pre>

{
   "name" : "MongoDB",
   "type" : "database",
   "count" : 1,
   "info" : {
               x : 203,
               y : 102
             }
}
</pre>

Java形式
<pre>
BasicDBObject doc = new BasicDBObject();

doc.put("name", "MongoDB");
doc.put("type", "database");
doc.put("count", 1);

BasicDBObject info = new BasicDBObject();

info.put("x", 203);
info.put("y", 102);

doc.put("info", info);
</pre>

<p>上のJSON形式のコードはすっきりしており一目で構造が把握できるのに対し、下のJavaのコードはひどいことになっています。長年Javaプログラマをやっている私でも「これはひどいな」と思います。噂ではMongoDBのJavaドライバに絶望してnode.jsに走ってしまったプログラマもいたとかいないとか。<p>

<p>しかしJavaは元々こういう言語ですから、仕方ありません。Javaにはこのようなコードで見える範囲ではなく、もっと他の所にたくさんの良さがありますので、Javaプログラマであればこの程度は我慢して使うのがよいでしょう。上記のJavaのコードは次のようにインデントさせてみたりすると、少しだけ構造を把握しやすくなるかもしれません。</p>

<pre>
BasicDBObject doc = new BasicDBObject();

doc.put("name", "MongoDB");
doc.put("type", "database");
doc.put("count", 1);

    BasicDBObject info = new BasicDBObject();

    info.put("x", 203);
    info.put("y", 102);

doc.put("info", info);
</pre>

<p>さて、このようにJavaでは単純なクエリーを組み立てるだけでも冗長な記述が必要となるので、対策として、ユーティリティ的なクラスを作っておき、よくあるパターンの問い合わせについての記述を極力減らせるようにするとよいかと思います。私はJDBCプログラミングでもユーティリティ的なクラスを頻繁に使うことで冗長な記述を避けるようにしていたため、この辺りの事情はJDBCのAPIの場合とまるっきり同じだなぁと感じました。</p>

<p>まだまだMongoDBでの開発経験が少ないため大した例を示すことができず恐縮ですが、例えば以下のようなメソッドなどがあると便利に使えます。基本的な狙いはDBObjectやBasicDBObjectといった記述を呼び出し側コードで省略できるようにする、ということです。</p>

<pre>
public static List getList( DBCollection coll, String key, Object value )
{
DBObject query = new BasicDBObject( key, value );
DBCursor cursor = coll.find( query );
List list = new ArrayList();
while( cursor.hasNext() )
	{
	list.add( cursor.next() );
	}
return list;
}
</pre>

<p>このメソッドは「key=value」という非常に単純な条件にマッチするドキュメントをすべて取得し、1つのListオブジェクトに格納して返すという動作をします（結果のドキュメント数が多すぎる場合にはメモリ不足を誘発する可能性があるコードとなっているので注意が必要です）。単純にコレクションにアクセスしたい場合にはこのメソッドを呼び出すだけで済みます。そのため呼び出し側のコード記述量が減り、またコードも読みやすくなります。</p>

<pre>
public static WriteResult updateOne( DBCollection coll, String queryKey, Object queryValue, String updateKey, Object updateValue )
{
DBObject query = new BasicDBObject( queryKey, queryValue );
DBObject newObj = new BasicDBObject( "$set", new BasicDBObject( updateKey, updateValue ) );
return coll.update( query, newObj, false, false, WriteConcern.SAFE );
}
</pre>

<p>このメソッドはごく単純なアップデートを行う場合に使用します。queryKeyがqueryValueの値を持つドキュメントの、updateKeyの値をupdateValueにする、というものです。<p>

<p>このように少し工夫することでコードの記述量を減らすことができるチャンスがありそうです。ただし、複雑な構造を組み立てる場合は、結局BasicDBObjectにせっせとputすることになりそうで、Javaならではの苦行となりそうですが...。</p>

<h3>オペレータは単純に文字列である</h3>
<p>MongoDBユーザならすぐにおなじみとなる「$gt」「$set」「$inc」などのオペレータは、Javaのドライバ内では単純にStringとして扱われます。<a href="http://api.mongodb.org/java/current/com/mongodb/QueryOperators.html">こちらのJavaDoc</a>にあるようにQueryOperatorクラス内のstaticフィールドとしていくつかが定義されていますが、これらの実装は単なるString型の変数であり、実装はずばり次のようになっています。</p>

<pre>
public class QueryOperators {
	public static final String GT = "$gt";
	public static final String GTE = "$gte";
	public static final String LT = "$lt";
	public static final String LTE = "$lte";
	public static final String NE = "$ne";
	public static final String IN = "$in";
	public static final String NIN = "$nin";
	public static final String MOD = "$mod";
	public static final String ALL = "$all";
	public static final String SIZE = "$size";
	public static final String EXISTS = "$exists";
	public static final String WHERE = "$where";
	public static final String NEAR = "$near";
}
</pre>

<p>私が「あれ？」と思ったのは、例えば「$inc」などはどこにも定義されていないことです。そのため、コードの中で積極的に上記の定義を使おうとしても、「$inc」などについては結局文字列として"$inc"と書いてしまう方が多いのではないかと思います。自分で新規にQueryOperatorsクラスのようなものを作って、そこに定義するのもひとつの手かもしれません。</p>

<p>特別な意味を持つオペレータが単純に文字列として扱われてしまうため、例えばユーザ入力に$がある場合などのエスケープが必要な場合なども想定されます。多くのアプリケーションではオペレータの部分にユーザ入力が入ってくることはないと思いますが、少し変わったことをやろうとしたときにセキュリティホールを作り込んでしまう可能性があるように感じました。せっかくのJavaなので、オペレータには独自のクラスが定義されており、型チェックが自動的に行われるような動作が行われればよかったのでは、と感じました（ドライバの実装上、何かしらの理由があって今のような形になっているのだろうと思いますが...）。</p>

<h3>MongoExceptionはRuntimeExceptionを継承している</h3>
<p>MongoDB関連の例外としてMongoExceptionクラスが定義されていますが、これはRuntimeExceptionを継承しています。つまり関数の定義において明示的にthrows MongoExceptionと記述する必要がありません。これはJavaにおいて賛否両論あるテーマとなっており、MongoDBがRuntimeExceptionを選んだのは、いかにも最近のソフトウェアらしい、と感じました。しかし私個人の好みはJDBCにおけるSQLExceptionのように必ず宣言する必要がある例外（checked exception）であったため、少し残念です。</p>

<h3>DBクラス（オブジェクト）は抽象的な扱いである</h3>

<p>MongoDBにおいて、JDBCプログラミングでのConnectionクラスに相当するのがDBクラスなのかと思っていたのですが、微妙に異なるようです。JDBCのConnectionクラスではcloseメソッドを呼ぶことで裏側にあるTCPコネクションが閉じられたりしますが、DBクラスにはそもそもcloseメソッドが存在しません（Mongoクラスに存在しています）。MongoDBではTCPコネクションレベルの、いわゆる「実際のコネクション」は抽象化されて扱われており、プログラマからは直接扱えないようになっています。</p>

<p>この抽象化は非常によくできており、たとえば一度MongoDBが落ちて再起動するようなことがあっても、ちょうどそのタイミングにJavaコード側からMongoDBにアクセスしていなければ、MongoDBが落ちたことに気づかずにコードを実行することができます。つまり、一度取得したDBクラスのインスタンスは、MongoDBの再起動後（バックグラウンドでTCPコネクションがすべて切断され、ドライバによって再び自動的に接続されなおした後）にもそのまま使用することができます。Mongoクラスのインスタンスについても同様で、「アクセスしてみたら、既にMongoDB側からcloseされていたので例外が投げられた」ようなことはなさそうです。これはJDBCに比べて良くできている点ではないかと思います。</p>

<h3>スレッドセーフである</h3>
<p>ドキュメントで明示的に「スレッドセーフである」と謳われており、安心してマルチスレッドな環境から使用することができます。</p>

<p>MongoDBでは書き込みが即座に（同時に接続している他のクライアントから見えるデータベースに）反映されるわけではないという性質がありますが、この挙動を自分のコード側から避けたい場合にはDBクラスのrequestStart()およびrequestDone()などを使用すればよいようです。この場合ドライバ内でThreadLocalクラスが使用され、書き込みとそれに続く読み込みが内部で同じコネクション（一貫したデータアクセスが提供され、書き込んだものは次の読み込みで見えるようになっている）によって実行されるようになります。</p>

<p>つまりドライバの実装は呼び出し側のスレッドを意識したコードとなっているので、requestStart()を呼び出した後に、DBクラスのインスタンスを別のスレッドに渡し、そちらでrequestDone()を呼び出す、ようなことをするとうまく動作しないと思われます（普通はそんなことはやらないと思いますが...）</p>

<h3>Replica Sets使用時の挙動</h3>
<p>私がMongoDBを選んだ理由のひとつが親切設計のレプリケーション機能です（具体的にはReplica Setsを利用しています）。Replica Sets使用時のJavaドライバの挙動について少し調べてみました。</p>

<h4>書き込み先を選ぶ必要はない</h4>
<p>Replica SetsではいくつかのMongoDBのノードのうち、プライマリに対して書き込みを行う必要がありますが、この「どれがプライマリで、どれがセカンダリか」の判断はドライバが行ってくれるため、アプリケーション側は一切気にする必要がありません。障害時にプライマリが切り替わった後も、ドライバが自動的にプライマリの変更などを検知し、アクセス先を変更してくれます。</p>

<h4>1つのノードを教えてやればOK</h4>
<p>アプリケーション側（具体的には、Mongoクラスのインスタンス）には複数あるReplica Setsのノードのうち、最低1つのノードの場所（IPアドレスあるいはホスト名、ポート番号）を教えてやれば、後は自動的にすべてやってくれます。</p>

<h4>セカンダリだけ教えてもOK</h4>

<p>アプリケーション側で意図せずにセカンダリの場所だけを教えてしまった場合でも、自動的にプライマリを見つけ、書き込みはそちらに行ってくれます。</p>

<h4>プライマリを教える方がよい</h4>
<p>ただし、前項に書いた「セカンダリの場所だけ教える」のは、可能であれば避けた方がよいようです。次のようなテストで、何度か例外が出ることを確認しました。</p>
<ul>
<li>Mongoクラスのインスタンスを新規に生成し、直後にデータ書き込みを行う</li>
<li>Mongoクラスのインスタンスのcloseを呼び出す</li>
<li>上記を繰り返す</li>
</ul>
<p>例外が出る原因について、半分は推測ですが、次のようなもののようです。</p>

<p>まず、Replica Sets使用時には、Javaドライバの内部で2つのスレッド（ReplicaSetStatus:UpdaterスレッドとMongoCleanerスレッド）が生成されます。これらのスレッド（のうち、おそらくReplicaSetStatus:Updaterの方）はMongoクラスのインスタンスが生成された直後に生成され、Replica Setsの状態を把握するためにMongoDBに接続し、プライマリやセカンダリなどがどこにいるのかを知り、それらにTCPコネクションを張ります。これには（おそらく1秒以下だと思われますが）時間がかかるため、この前に書き込みが行われてしまう場合にエラーが出ることがあるようです。</p>

<p>プライマリ側のアドレスのみを教えた場合には上記テストでエラーは出なかったので、プライマリ側のアドレスを教えるようにするのがよいでしょう。また、Mongoクラスのインスタンスを作成したら1秒ほどスリープさせるという方法でもエラーが出なくなったので、そのような方法もよいかもしれません。</p>

<p>多くのアプリケーションではMongoクラスのインスタンスは最初に1度作ったきり、それを使い続けると思います。そのような場合はこの問題についてあまり神経質になる必要はありません。</p>

<h4>コンストラクタでレプリケーション種別を実質的に指定することになる</h4>
<p>Javaアプリケーション側でMongoクラスのインスタンスを生成する際のコンストラクタの引数によって、どのレプリケーションを使用するか（あるいはレプリケーションを使用しないか）を指定することになります。このこと自体は慣れてしまえば何でもないのですが、初めての場合はわかりにくいかもしれません。<a href="http://api.mongodb.org/java/current/com/mongodb/Mongo.html">MongoクラスのJavaDoc</a>にわかりやすいコード例があるので、具体的な記述方法についてはそちらを参照ください。</p>

<p>ひとつ覚えておくべきことは「Javaのコード側で明示的にレプリケーションのタイプを指定することになる」ということです。今までスタンドアローンのMongoDBにアクセスしていたアプリケーションをReplica Setsにアクセスするようにするためには、コンストラクタのコードを変える必要があります。変えない場合は、プライマリが移動した際に追いかけてくれません。</p>

<p>また、逆の場合（Replica SetsにアクセスしていたコードをスタンドアローンのMongoDBにアクセスさせる場合）はそのままでも動作してくれるようです。</p>

<h4>コネクションがリークする場合がある</h4>
<p>これは単純にバグだと思うのでここに書いているよりも報告したほうが良いのですが、見つけたので書いておきます。Mongoクラスのインスタンス1つに対して1つのコネクションがリークするだけの問題なので、普通はまったく気にする必要がないバグです。</p>
<p>Replica Setsではホスト名とIPアドレスなどが区別されます。Replica Setsを構成する際にホスト名（例えばnode1.example.jpなど）を設定情報として持っているにも関わらず、Javaコード側（具体的にはMongoクラスのインスタンスを生成する際のコンストラクタに渡す引数）にホスト名ではなくIPアドレスを渡してしまうと問題が起こります。ドライバははじめにそのIPアドレスで指定されたノードに接続し、Replica Setsの情報を取得しますが、その後さらに設定情報に基づいてホスト名ベースでのTCPコネクションを新規に取得します。</p>
<p>しばらく後に、Mongoクラスのインスタンスのclose()メソッドを呼び出した際、これらのホスト名ベースによって取得されたTCPコネクションは正しく破棄されますが、最初に作成されたIPアドレスベースのコネクションがcloseされず、netstatなどで状態を見るとEstablishedのまま残ってしまうことが確認できます。</p>
<p>先述したようにMongoクラスは普通一度作ったらそれっきり、という場合が多いと思います。この場合はこのコネクションリークは問題にはなりません。ただし、MongoDBにそれほど頻繁にアクセスを行うわけではないJavaアプリケーション（特にサーバなど）で、毎回アクセスするたびにMongoクラスのインスタンスを作り直しているような場合には深刻なリソースリークに繋がるおそれがあるので注意が必要です。</p>

<h4>スレッドが2つ生成される</h4>
<p>先ほどから書いているようにReplica Setsを使用する場合にはドライバ内部でスレッドが2つ生成されるので、Mongoクラスのインスタンスの作成はリソース的にはやや重い処理になります。ループ内部でMongoクラスのインスタンスを大量に作成するようなことは避けるようにしましょう（そんなことはまず行われないと思いますが...）。</p>

<h3>動作環境など</h3>
<p>はじめてのMongoDB/Javaアプリ開発を経験して感じたこと、気づいたことなどを簡単にまとめてみました。おそらく間違っている点や不足している点も多々あるかと思いますので、フィードバックを@kinyukaにいただければ幸いです。</p>

<p>なお、動作確認を行った環境はLinux X86_64、Java 1.6.0_22、MongoDB 1.8.1、Java-Mongoドライバー2.6.3です。</p>

]]></content>

</entry><entry>
<title>MongoDBとメモリ使用量</title>
<link rel="alternate" type="text/html" href="/information/waf_tech_blog/2011/07/waf-blog-001.html" />
<id>WTB-7_20110705130000</id>
<published>2011-07-05T04:00:00Z</published>
<updated>2011-07-05T04:00:00Z</updated>
<summary>Scutum（スキュータム）ではサービス開始時より、データストアとしてmemcachedとpgpool II+PostgreSQLを利用しています。最近になってより柔軟にデータを取っていきたいというニーズが高くなってきたため、MongoDBの導入を行いました。
Linux(X86_64）上でMongoDBを動作させたときのメモリの消費について、簡単にですが調べてみました。</summary>
<author>
<name>金床　“Kanatoko”</name>
</author>

<content type="html" xml:lang="ja" xml:base="http://www.scutum.jp/information/waf_tech_blog/">
<![CDATA[<img src="/information/waf_tech_blog/images/PoweredMongoDBblue50.png">
 <h3>はじめに</h3>

<p><a href="http://www.scutum.jp/index.html" target="_blank">WAF「Scutum（スキュータム）」</a>ではサービス開始時より、データストアとしてmemcachedとpgpool II+PostgreSQLを利用しています。これらはどれも安定して動いており満足しているのですが、最近になってより柔軟にデータを取っていきたいというニーズが高くなってきたため、MongoDBの導入を行いました。まだ完全なリプレースまでは至っていませんが、元々のデータベースのスキーマ構造がシンプルであることもあり、数ヶ月以内にはpgpool II+PostgreSQLの部分をMongoDB（Replica Sets）で置き換えることができるのではないかと思っています。</p>

]]><![CDATA[<h3>MongoDBにとっての「メモリ使用量」</h3>

<p>MongoDBを導入するにあたり、Linux(X86_64）上でMongoDBを動作させたときのメモリの消費について、簡単にですが調べてみました。まず参考にしたのは本家のドキュメントです（<a href="http://www.mongodb.org/display/DOCS/Checking+Server+Memory+Usage" target="_blank">英語</a>、<a href="http://www.mongodb.org/pages/viewpage.action?pageId=21267688" target="_blank">日本語訳</a>）。読んでみたところ、非常にあっさりしているな、という印象を受けました。例えば私が慣れているPostgreSQLでは、メモリをどのくらい割り当てるかについてはいくつかの設定項目があり、サーバ上でpostgresプロセス間で共有するメモリや、ソート時にワークスペースとして使うメモリの量などを指定することができます。MongoDBでは基本的にそのような項目が存在せず、デフォルトの状態におまかせ、とするのが作法のようです。しかしそれでは巨大な（インデックスの張られていない）データをソートしようとしたりした場合に何が起こるのかが心配です。まさかサーバのメモリを圧迫してOOM Killerが発動するのでは？という不安が浮かんだのですが、なんとMongoDBはそのような場合にはソートをあっさりあきらめるという動作をするようです。「ちゃんとインデックスを張ってね」あるいは「メモリが少なくてもソートできるように結果の件数を絞ってね」というようなエラーメッセージが出されることになり、メモリを無駄に消費することにはなりません。NoSQL時代を感じさせる、割り切った仕様だと感じます。</p>

<p>そのような背景もあり、MongoDBでは、メモリ使用量が話題になる（問題になる）のは「サイズの大きなデータベースを総なめするようなアクセスが発生した場合に、メモリをどれだけ圧迫するか？」といった視点となることが多くなっています。</p>

<h3>Memory Mapped File</h3>

<p>ここで登場するのが先述した本家のドキュメントにも記述されている、Memory Mapped File（以下mmap）という概念です。mmapを使うと、メモリにアクセスするようなコードを書くだけで、それが自動的にファイルへのアクセスとなります。「あ...ありのまま 今　起こった事を話すぜ！...オレはメモリに書き込んでいると思っていたら、いつのまにかファイルに書き込んでいた」となり、恐ろしいものの片鱗を味わうことができます。OS全体でメモリが足りなくなった場合には、OSが自動的にmmapで使われているメモリを回収して他の用途に使ってくれるため、大きなファイルに対してmmapを行っても、基本的には大丈夫ということになっています（ただし、MAP_LOCKEDを指定してmmapを呼び出した場合には回収されません）。</p>

<p>私はLinuxのmmapでは<a href="http://kanatoko.wordpress.com/2011/01/30/vmware-server-performance/" target="_blank">VMware Serverで過去に一度痛い目に遭っており、</a>今回はそのときの経験が生きることになりそうです。まず気をつけるべきこととして、「mmapで消費しているメモリはfreeコマンドでは見えない」ということがあります。実際にmongodプロセスのmmapがどれだけのメモリを消費しているかを調べるためには、/proc/PID/statusのVmRSS項目を見るのが確実です。また、OS全体でmmapなどによって消費されているメモリの量は、/proc/meminfoのMapped項目で知ることができます。</p>

<p>straceを使ってmongodプロセスを追ってみると、以下のようにmmapを呼び出していることが確認できます。</p>

<pre>
open("/data/db/test1.0", O_RDWR|O_NOATIME) = 6
lseek(6, 0, SEEK_END)                   = 67108864
lseek(6, 0, SEEK_SET)                   = 0   
mmap(NULL, 67108864, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x7facc0000000
</pre>

<p>呼び出しの際の引数として、先述したMAP_LOCKEDは使用されていません。そのため、ここで割り当てられたメモリはOSが必要に応じて自動的に回収してくれることになります。</p>

<h3>ディスクキャッシュとmmapの違い</h3>

<p>私がMongoDBのメモリ使用量についてググっていた中で最も興味深かったのが<a href="http://groups.google.com/group/mongodb-user/browse_thread/thread/2646a52c4f41d832/c5579706d733c21a" target="_blank">こちらのスレッド</a>です（以下、かなり意訳なので、時間がある方は英文をお読みください）。</p>

<p>「開発環境でMongoDBを使っていると、だんだん（おそらくOS全体が）重くなってしまい、そのうちどうしようもなくなる。mongodがメモリを消費しすぎているからだ。mongodを再起動すると解決する。その繰り返しでうんざりする」</p>

<p>というような質問者に対して、</p>

<p>「それはMongoDB固有の問題ではない。あらゆるデータベースにおいて、大きなデータを総なめするケースで常に発生する問題だ。」</p>

<p>という回答がなされています。MySQLやPostgreSQLなどでも、データが大きければディスクアクセスが発生し、その際にページキャッシュという形でメモリを消費します。これがMongoDBにおけるmmapによるメモリ消費と同じだろうという主張です。私はこれが本当なのかどうか非常に気になったため、手元の環境でテストしてみることにしました。</p>

<h3>2通りのメモリ消費を比較テスト</h3>

<p>テストの目的は以下になります。</p>

<ul>
<li>大量にmmapが行われた場合、メモリが圧迫されるが、そのとき他のプロセスにどのような影響が出るか</li>
<li>大量にディスクアクセスが行われた場合、メモリはページキャッシュによって圧迫されるが、そのとき他のプロセスにどのような影響が出るか</li>
</ul>

<p>前者のテストは2通りの方法で行いました。まず、単にmmapを呼び出すだけの簡単なプログラムをCで作成し、それを使う方法。次に、実際にmongodプロセスに対して、インデックスを使わない大量のデータアクセスを行わせる方法です。後者のテストについては、MongoDBのデータファイルをcatして/dev/nullに流すという方法で行いました。</p>

<p>結果わかったことは以下の通りです。</p>

<ul>
<li>mmapによるメモリ圧迫の方が、ページキャッシュによる圧迫よりも、他のプロセスに与える影響（=スワップアウトさせる）が大きい</li>
<li>特に、/proc/sys/vm/swappinessの値が60などのように大きな場合、顕著となる</li>
</ul>

<p>具体的な数値は次のようになります。まず、実験を行ったサーバは4GBのメモリを載せたX86_64のUbuntuで、カーネルは2.6.32です。ここでmallocによってヒープに2GBのメモリを確保するだけの無駄なプロセスを起動しておきます（ヒープには書き込みを行っておき、実際にメモリが消費されることを確認します）。</p>

<p>mmapのテストを行います。あらかじめ用意しておいた10GBほどのデータを持つデータベースに対してmongodを起動し、mongoコマンドで接続した後に、先述したとおりにインデックスが効かないデータ総なめのアクセスを行わせます。すると、</p>

<ul>
<li>swappinessが60の場合、ヒープにメモリを持っていたプロセスを中心に、1.3GBのスワップが発生します。</li>
<li>swappinessが10の場合には、わずか80MBのスワップで済みました。</li>
</ul>

<p>次にページキャッシュのテストです。/data/db/にあるMongoDBのデータファイルをcatして/dev/nullにリダイレクトします。すると、</p>

<ul>
<li>swappinessが60の場合、2MBのスワップが発生しました。</li>
<li>swappinessが10の場合、スワップは一切発生しません。</li>
</ul>

<h3>結論</h3>

<p>以上のように、MongoDBが派手なデータアクセスを行う場合、他のプロセスをスワップアウトさせる可能性が高いです。そのための対策として、以下のようなことが考えられます。</p>

<ul>
<li>メモリを可能な限りたっぷり載せる</li>
<li>MongoDBが効率的なアクセスを行うよう、インデックスの使用が正しくなされているか日々チェックしておく</li>
<li>MongoDBのメモリ使用量（/proc/PID/statusのVmRSS）を日々監視しておく</li>
<li>MongoDBのデータベースのサイズをできるだけ低く保つ</li>
<li>/proc/sys/vm/swappinessを低い値にする</li>
</ul>

<p>スワップ領域を用意しなければそもそもスワップアウトされることもないので、それでもよいかもしれませんが、万が一でもOOM Killerが発動すると怖いので、私はswappinessを10にした上でスワップ領域を割り当てておくつもりです。</p>


]]></content>

</entry>
</feed>

