Tomcat7の新機能(CSRFとメモリリーク)

巷でうわさのTomcat7ですが、新しい機能として

  • Generic CSRF protection
  • Web application memory leak detection and prevention

という気になる機能が入っているね、とT2チーム内で話題になっていました。
Tomcatのサイトを見るとバイナリ版配布が開始されていたので、とりいそぎJadってみることにしました。

Generic CSRF protection

CSRFについては高木先生などの専門家に解説をまかせますが、要はサイト外からのリクエストによって不正な処理が行われてしまう、というセキュリティホールです。(リンクを押すと、mixiに勝手に書き込まれてしまうとかありましたよね)
これを防ぐには、リクエストデータがサイト外からきているかどうかを判定する(=リクエストがサイト内から来ていることを判定する)ことが必要ですが、Tomcat7ではこれをフィルター、セッション、リクエストパラメータを使って実現していました。
実際のコードは、「org.apache.catalina.filters.CsrfPreventionFilter」ですが、かいつまんで書くとこういう感じになります。

public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        boolean skipNonceCheck = false;
        //全部にチェック入れたら初回リクエスト以外全部通らないので、
        //スキップするパスかどうかチェック
        if (Constants.METHOD_GET.equals(req.getMethod())) {
            String path = req.getServletPath();
            if (req.getPathInfo() != null) {
                path = path + req.getPathInfo();
            }
            
            if (entryPoints.contains(path)) {
                skipNonceCheck = true;
            }
        }
        //トークンの入れ物をセッションから取得
        LruCache<String> nonceCache =
            (LruCache<String>) req.getSession(true).getAttribute(
                Constants.CSRF_NONCE_SESSION_ATTR_NAME);
        
        if (!skipNonceCheck) {
            String previousNonce =
                req.getParameter(Constants.CSRF_NONCE_REQUEST_PARAM);
            //トークンの入れ物があって、パラメータのトークンが入ってない場合はエラー
            if (nonceCache != null && !nonceCache.contains(previousNonce)) {
                res.sendError(HttpServletResponse.SC_FORBIDDEN);
                return;
            }
        }
        //トークンの入れ物がない場合は新規作成
        if (nonceCache == null) {
            nonceCache = new LruCache<String>(nonceCacheSize);
            req.getSession().setAttribute(
                    Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonceCache);
        }
        //ランダムなトークンを生成
        String newNonce = generateNonce();
        //入れ物にセット
        nonceCache.add(newNonce);
        //encodeURL,encodeRedirectURLが呼ばれたときに、
        //トークンをパラメータにひっつけるResponseWrapper
        wResponse = new CsrfResponseWrapper(res, newNonce);
    } else {
        wResponse = response;
    }
    
    chain.doFilter(request, wResponse);
}

(2010年7月15日時点のTrunkから引っ張ってきたコードに修正しました)


URLのリクエストパラメータにトークンを引っ付けておいて、セッションに格納しておいたトークンの一覧にあるかどうかを比較するということをしています。StrutsのTransactionTokenのような感じですね。これを行っておけば、サイト外からのリクエストはトークンがついていないためはじくことができる、という寸法です。
単純ですが、効果はありますね。

Web application memory leak detection and prevention

こちらはWebアプリのメモリリークを検知・防止する、という機能ですが、該当すると思われる機能が
「org.apache.catalina.core.JreMemoryLeakPreventionListener」、「org.apache.catalina.loader.JdbcLeakPrevention」、「org.apache.catalina.core.StandardHost」に実装されていました。

org.apache.catalina.core.JreMemoryLeakPreventionListener

このクラスはTomcatのLifecycleListenerとして実装されていて、初期化時(Lifecycle.INIT_EVENT発生時)に、以下5つの項目に対してメモリリークを防止できるようになっています。

  • appContextProtection
  • gcDaemonProtection
  • tokenPollerProtection
  • urlCacheProtection
  • xmlParsingProtection


実際のコードは、次のようになっています。

  if (appContextProtection) {
      ImageIO.getCacheDirectory();
  }
  if (gcDaemonProtection) {
      try {
          Class<?> clazz = Class.forName("sun.misc.GC");
          Method method = clazz.getDeclaredMethod("requestLatency",
                  new Class[] {long.class});
          method.invoke(null, Long.valueOf(3600000));
      } catch (ClassNotFoundException e) {
          if (System.getProperty("java.vendor").startsWith("Sun")) {
              log.error(sm.getString(
                      "jreLeakListener.gcDaemonFail"), e);
          } else {
              log.debug(sm.getString(
                      "jreLeakListener.gcDaemonFail"), e);
          }
      } catch (Exception e) {
          log.error(sm.getString("jreLeakListener.gcDaemonFail"), e);
      }
  }
  if (tokenPollerProtection) {
      java.security.Security.getProviders();
  }
  
  if (urlCacheProtection) {
      try {
          URL url = new URL("jar:file://dummy.jar!/");
          URLConnection uConn = url.openConnection();
          uConn.setDefaultUseCaches(false);
      } catch (Exception e) {
          log.error(sm.getString(
                  "jreLeakListener.jarUrlConnCacheFail"), e);
      }
  }
  if (xmlParsingProtection) {
      DocumentBuilderFactory factory =
          DocumentBuilderFactory.newInstance();
      try {
          factory.newDocumentBuilder();
      } catch (ParserConfigurationException e) {
          log.error(sm.getString("jreLeakListener.xmlParseFail"), e);
      }
  }

ぱっと見た感じでは意味不明なコードですが、どうやらJREのクラスのうち、内部でキャッシュ処理を行うようなものやスレッドを起こすものについて先に処理を呼び出して、各Webアプリケーションのクラスローダーでロードさせない、という意図のようです。
実際のコードには理由がいろいろコメントで書いてあるので、一度読んでみると面白いと思います。
これらはリーク予防のためみたいです。

org.apache.catalina.loader.JdbcLeakPrevention

このクラスは、WebappClassLoaderの終了時に呼ばれます。呼ばれる処理は以下のような内容です。

public List<String> clearJdbcDriverRegistrations() throws SQLException {
    List<String> driverNames = new ArrayList<String>();

    // This will list all drivers visible to this class loader
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
        Driver driver = drivers.nextElement();
        // Only unload the drivers this web app loaded
        if (driver.getClass().getClassLoader() !=
            this.getClass().getClassLoader()) {
            continue;
        }
        driverNames.add(driver.getClass().getCanonicalName());
        DriverManager.deregisterDriver(driver);
    }
    return driverNames;
}

コードからすると、WebアプリがロードしたJDBCドライバをアンロードする、というような内容になっています。
DriverManagerは親クラスローダーで読まれているので、そこに各Webアプリから登録したドライバーがリークしてしまうのでderegisterDriverしましょう、ということなのだと思います。
こちらもリーク予防機能に該当すると思われます。

org.apache.catalina.core.StandardHost

StandardHostのほうは、リーク検知機能がついています。StandardHostには以下のクラスとメソッドが定義してあります。

private class MemoryLeakTrackingListener implements LifecycleListener {
    @Override
    public void lifecycleEvent(LifecycleEvent event) {
        if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
            if (event.getSource() instanceof Context) {
                Context context = ((Context) event.getSource());
                childClassLoaders.put(context.getLoader().getClassLoader(),
                        context.getServletContext().getContextPath());
            }
        }
    }
}
public String[] findReloadedContextMemoryLeaks() {
    
    System.gc();
    
    List<String> result = new ArrayList<String>();
    
    for (Map.Entry<ClassLoader, String> entry :
            childClassLoaders.entrySet()) {
        ClassLoader cl = entry.getKey();
        if (cl instanceof WebappClassLoader) {
            if (!((WebappClassLoader) cl).isStarted()) {
                result.add(entry.getValue());
            }
        }
    }
    
    return result.toArray(new String[result.size()]);
}

MemoryLeakTrackingListenerでは、Webアプリ開始後にchildClassLoadersというWeakHashMapに、クラスローダーを登録しています。
このクラスローダーは、各WebアプリのWebappClassLoaderになります。
findReloadedContextMemoryLeaks()内では、childClassLoadersの中のクラスローダーのうち、isStarted()がfalseのものを列挙しています。
WebappClassLoaderは停止時にisStarted()をfalseにして、内部のクラスを破棄します。
childClassLoadersはWeakHashMapで実装されているため、クラスローダーが正常に破棄されていればこのchildClassLoadersからも削除されますが、メモリリークした場合は残っているため、isStarted()がfalseのクラスローダー=メモリリークを起こしてるクラスローダー、という扱いをしていると思われます。
こちらはリーク検知機能ですね。

まとめ

CsrfPreventionFilterは、Tomcatに限らず普通のWebアプリでも使えると思うので、活用したらよいと思います。
メモリリークのほうは、検知機能と書いてあったので、たとえば一定期間のメモリ量を記録して増加傾向があると報告する、みたいな機能かと思いましたが、どうもそうではないようです。
(とはいえまだコード全部見てないので、もっとすごい機能が隠されているのかもしれませんが)
ただ、JreMemoryLeakPreventionListenerで行っている先読み処理や、JDBCドライバのderegisterDriver処理は、何かあったときに解決の糸口になるかもしれませんので、知っておいて損はないですね。