Programming SHOT BARへようこそ。今回はWebアプリケーションでのセッション管理の際の防衛的プログラミングです。
数日前にASP.NETでのセッション管理の話題がわんくま内で盛り上がっていましたが
(
けろ様のページがまとめになっているのかな?)、
.NET系は詳しくないのであまり口が挟めず…。
私の理解ではJavaの場合のセッション管理と概念は同じ、しかし、実装が違うという程度のようなので
本質的な問題は一緒なのではないかと捉えています(@ITの
記事を参考にしました)。
とりあえず、この稿ではJavaの大規模開発でセッションを扱う場合の防衛策に注視してお送りします。
セッションに値を格納するには
以下はセッションに値を格納する簡単なサンプルプログラムです。
public class SampleServlet extends HttpServlet {
/**
* @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
session.setAttribute("test", new Object());
}
}
HttpServletRequestオブジェクトからセッションを表すHttpSessionオブジェクトを取得していることがわかりますね。
これは、HttpServletRequestオブジェクトを参照できるあらゆる場所からセッションを操作される
可能性があることを意味しています。
セッションには無秩序になんでも格納すればよいというものではありません。
格納するのは使うことを想定して格納するわけです。
ところが、無秩序に格納してしまうと、どこで格納したものを参照しようとしているのか、
わからなくなってしまいます。
格納されているデータがどうもおかしい、となれば、出所を突き止めなければなりません。
しかし、誰もが自由にセッションでデータを置けるのだとしたら、
誰が置いたデータなのか出所が不明になってしまうわけです。これでは困ります。
人的運用でのセッションの管理の限界
業務でシステムを作る場合、少なくともどういうキーでどこのモジュールが何を格納するのか、
そして、それを参照するのはどこなのか(汎用データの場合は参照元は管理しないことが多いでしょう)
といったことを書類で管理します。
しかし、書類でしか管理されていない場合、ひっそりとセッションに情報を格納しても、
何かよほどの機会でもない限り、発覚しないわけです。
しかも、大規模なシステム開発というのは100人以上の人が入り乱れて開発していたりしますから、
「セッションへの値の格納には書面で報告し、許可を得ること」
とお触れを出したところで行き渡らないこともあります。
全体会議を開いて注意事項を述べたとしても、人の入れ替わりも多いですから、
それ以後にプロジェクトに参加した人は「聞いていないよ、そんなこと」となってしまうこともあります。
人に対する啓蒙活動を無駄だとは言いませんが、確実ではないことは(残念ながら)間違いないことなのです。
プログラムで防衛を試みる
人に守らせるという根性論ではうまくいかないとなれば、プログラムで機械的にはじく方法論を考えることになります。
故意にせよ、事故にせよ、管理外のセッション操作は検出して警告しないといけません。
しかし、HttpSessionオブジェクトはHttpServletRequestから取得できるわけですし、
HttpServletRequestはインターフェースで実装はサーブレットコンテナによって作られ、
Servletの引数に渡される代物なのです。
突破口はjavax.servlet.Filterにあります。
FilterはServletの処理の前に噛ませることが出来る、文字通りフィルタなのです。
ここでHttpServletRequestをGoFデザインパターンのProxyパターンで
セッション操作に対して警告するように細工したHttpServletRequestに差し替えれば
システム全体でセッションをいじろうとした際に警告することが出来ます。
まずFilterでは
public class ReadOnlySessionFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(new HttpServletRequestProxy((HttpServletRequest) request), response);
}
public void init(FilterConfig config) throws ServletException {
// TODO
}
public void destroy() {
// TODO
}
}
といったように、chain.doFilter()の引数の時点でHttpServletRequestに細工します。
HttpServletRequestの実装は
public class HttpServletRequestProxy implements HttpServletRequest {
private HttpServletRequest request;
public HttpServletRequestProxy(HttpServletRequest request) {
this.request = request;
}
/** ラップして返す */
public HttpSession getSession() {
return new HttpSessionProxy(this.request.getSession());
}
// 以下各メソッドをthis.request.xxx()として委譲
と細工したHttpSessionを返すようにし、HttpSessionの実装では
public class HttpSessionProxy implements HttpSession {
private HttpSession session;
public HttpSessionProxy(HttpSession session) {
this.session = session;
}
public void setAttribute(String arg0, Object arg1) {
throw new UnsupportedOperationException(
"管理クラス以外からのセッション操作は禁じられています");
}
/** 同一パッケージ内の管理クラスから設定する場合に用いる迂回路 */
void setAttributeInner(String arg0, Object arg1) {
this.session.setAttribute(arg0, arg1);
}
// 以下各メソッドをthis.session.xxx()として委譲
といったように、UnsupportedOperationExceptionをthrowするように仕込んでおきます。
このままでは、セッションになにも格納できないのでアクセスレベルをパッケージプライベートにした
setAttributeInner()を作っておいて、管理クラスからのアクセスだけを受け付けるようにします。
このような工夫をすることで、セッションへのデータの格納を検出し、警告することが出来ます。
問題点
このような細工をしてしまうと、フレームワークなどで暗黙にセッションにデータを格納したり
しているようなケースがあると動いてくれません。使えないケースもあると思います。
また、サンプルではざっくり省略していますがHttpServletRequestもHttpSessionも
メソッド数の多いインターフェースであるため、Proxyクラスを作るのは結構大変です。
また、サンプルでは省略していますが、本当は他にもオーバーライドする必要のあるメソッドがあります。
このようなクラスは開発時には意義がありますが、本番運用時には不要であるため、
テストの段階で取り除いてやる必要があるでしょう。
大規模システムとなると、技術力も理解もさまざまな人が入り乱れて開発しますから、
こういった防衛策を地味に重ねることがトラブルを未然に防ぎ、全体の品質を向上させ、
開発効率を高めることとなります。
投稿日時 : 2007年10月2日 14:24