セキュアな Web アプリケーションの作成

このセクションでは、Web アプリケーションのセキュリティを強化するコードを書く場合に利用できるテクニックを説明します。セキュリティ問題の詳細については、 「リソース」 にリストされているリソースを参照してください。

クライアントによるなりすましの防止

IP アドレスと HTTP ヘッダーは、ホストベースの認証を実行するためによく使用されます。たとえば、次の例のように、Referer ヘッダーやクライアントの IP アドレスをチェックして、リクエストが信頼できるソースからのものであるということを確認できます。

...
String remoteaddr = request.getRemoteAddr();
if (remoteaddr.startsWith("10.")) {
  System.out.println("リクエストを送信したホストは認証されました");
  chain.doFilter(request, response);
} else {
  RequestDispatcher rd = request.getRequestDispatcher("/errorPages/
forbidden.jsp");
  context.log("リクエストを送信したホストは認証されませんでした");
  rd.forward(request, response);
}
...

Referer などのリクエストヘッダーは簡単に偽造できます。クライアントは、ヘッダーを設定したり IP アドレスを偽造したりすることで、別のものになりすますことができます。

クライアントのなりすまし問題の解決策は、HTTP ヘッダーデータを認証メカニズムとして使用しないことです。

サンプルサーブレットを表示するには、samples JRun サーバを起動し、ブラウザで http://localhost:8200/techniques を開きます。

ステート情報の維持

ステート情報を提供する一般的な方法は、クエリ文字列パラメータ、非表示のフォームフィールド、または Cookie を使用して、クライアントサイドのステート情報を保管することです。たとえば、userID パラメータをクエリ文字列に追加し、その userID を Cookie に保管するか、または userID を非表示のフォームフィールドとしてページに含めます。クライアントが信頼できるものでなく、ステート情報に簡単にアクセスできるため、これらの方法はいずれも攻撃の対象になる可能性があります。

クラッカーは、多くの場合、クエリ文字列パラメータを使用して、Web アプリケーションの弱点をテストします。たとえば、クライアントが次の URL をリクエストしたとします。

http://www.hamsteak.com/accountBalance?userID=42

クライアントはリクエストを送信する前に、userID パラメータの値をブラウザで自由に変更できます。

HTML リクエストを生成するクライアントは、非表示のフォームフィールドを偽造できます。ブラウザに表示された Web アプリケーションの HTML ページのソースを表示するだけで、非表示のフォームフィールドは見つかってしまいます。クライアントは、どのようなリクエストも自由に作成できるので、リクエストを送信する前に、リクエスト文書の非表示のフォームフィールドを追加または削除できます。

Cookie は、通常クライアントに保管されたプレーンテキストファイルなので、悪質なデータを混入できます。さらに、クライアントは、リクエストの実行前に HTTP の Cookie ヘッダーを変更できるので、Cookie の存在は、信頼性を示す決定的証拠にはなりません。

次のテクニックを利用すれば、ステート情報を維持しながら、アプリケーションの信用を損なう危険性を軽減することができます。

クライアントサイドでの検証

フォームを不正なデータ入力から保護するには、クライアントサイドの検証を信用しないでください。JavaScript や VBScript でフォームフィールド検証を実装するのは、一般的な方法です。たとえば、電話番号に数値しか入力できないようにしたり、フォームフィールドに不正な文字を入力できないようにすることができます。ただし、ユーザーがクライアントアプリケーションでこれらのスクリプト言語をオフにできるので、クライアントサイドの検証が行われることを前提にすることはできません。

ダイナミック Web ページでクライアントが提供したデータを使用する場合は、 「サーバサイドでの検証」 で説明するように、サーバサイド検証や文字列操作を使用して、最初にデータを検証し、有害なコードを取り除く必要があります。

データベースの保護

データベースクエリでは、ユーザーが入力データをそのまま使用できないようにします。たとえば、次の例で示されているように、姓などのキーワードを入力するようにユーザーに要求し、そのキーワードを自分のクエリで使用するようにします。

String lname = request.getParameter("lastname");
sqlstmt = "SELECT firstname, lastname FROM users WHERE lastname = 
¥"lname¥"";

検証がない場合、ユーザーはページを送信する前に、クエリ文字列やフォームフィールドの値を自由に変更できます。アタッカーが使用するテクニックの 1 つは、悪質なクエリをフォームフィールドに挿入することです。これを SQL インジェクションアタックと呼びます。たとえば、ユーザーが次のクエリをフォームフィールドに追加することがあります。

jones; select * from users;

データベースによっては、クエリが両方とも実行されることがあります。

ユーザーが入力したデータをそのままデータベースクエリに追加する必要がある実装では、次のテクニックを 1 つ以上試みて、不正なクエリの処理を防止します。

サーバサイドでの検証

このセクションでは、クライアントによるダイナミックページへの有害コードの挿入を防止するテクニックを説明します。

文字列の操作

String クラスの標準メソッドを使用して、長すぎる入力を切り詰めたり、ユーザー入力から不正な文字を削除できます。ネガティブなフィルタリング (不正な文字をフィルタ) とポジティブなフィルタリング (正しい文字以外はすべてフィルタ) を使用できます。

次のネガティブなフィルタリングの例では、入力から & 文字と * 文字を削除して、下線に置き換えています。

String filteredQuery = req.getParameter("rogueQuery");
...
//不正なクエリを切り詰めます。
if (filteredQuery.length()>20) {
  filteredQuery = filteredQuery.substring(0,20);
}
//不正なクエリにある不正な文字を有効な文字で置き換えます。
char good = '_';
char bad[] = new char[2];
bad[0] = '&';
bad[1] = '*';
for (int i=0; i<bad.length; i++) {
  filteredQuery = filteredQuery.replace(bad[i],good);
}
...

Perl 言語で有名になった正規表現も、値の置換や変換に利用できる強力なツールです。次のリソースでは、Java の正規表現メソッドへのアクセスが提供されます。

HTML コードの除去

クライアントによるダイナミック Web ページへのコンテンツの追加を許可すると、サーバはデータの処理方法を管理できなくなります。ユーザーによって送信されたコンテンツは、サーバコードを攻撃したり、他のクライアントのコンピュータに転送されて表示される危険性があります。クライアントデータはすべて信頼できないと見なして、ユーザーが送信したデータは、サーブレットや JSP に取り込む前に無害なものに変換する必要があります。

たとえば、ユーザーが Web サイトに関するコメントを追加できるようにする場合は、HTML や JavaScript で使用される特殊文字を除去して HTML エンティティで置き換えるコードを使用する必要があります。HTML コードをクライアントブラウザに送信されるようにして、Web サーバでは処理されないようにします。

次のコード例では、クライアントの入力から <、>、&、" の文字を探し、これらの文字を表わす HTML エンティティに置き換えています。

...
public static final char lt = '<';
public static final char gt = '>';
public static final char amp = '&';
public static final char quot = '"';
...
filteredHTML = htmlCodeFormat(rogueHTML);
...
public String htmlCodeFormat(String filteredHTML) {
  int thisHtmlLength = filteredHTML.length();
  StringBuffer thisText = new StringBuffer(Math.round(thisHtmlLength * 
1.5f));
  for(int count = 0; count <thisHtmlLength; count++) {
    char thisChar = filteredHTML.charAt(count);
    switch (thisChar) {
      case lt:
        thisText.append("&lt;");
        break;
      case gt:
        thisText.append("&gt;");
        break;
      case amp:
        thisText.append("&amp;");
        break;
      case quot:
        thisText.append("&quot;");
        break;
      default:
        thisText.append(thisChar);
        break;
    } //switch の終わり
  } //for の終わり
  return thisText.toString();
}
...

サンプルサーブレットを表示するには、samples JRun サーバを起動し、ブラウザで http://localhost:8200/techniques を開きます。

HTML エンティティのリストについては、http://www.ramsch.org/martin/uni/fmi-hp/iso8859-1.html をご覧ください。

CERT advisory CA-2000-02 には、信頼できないコンテンツによる Web サイトの悪用を防止するための処置が記載されています。詳細については、http://www.cert.org/tech_tips/malicious_code_mitigation.htmlhttp://www.cert.org/advisories/CA-2000-02.html をご覧ください。

データベースのアクセス許可の設定

Web アプリケーションのデータを保護するには、クライアントから不正なクエリ出力が返されないようにするだけでなく、不正なクエリが実行されないようにしてください。クライアントの関心は必ずしも結果を表示することではなく、結果を表示せずにデータベースを破壊するクエリを送信することにある場合もあるからです。

たとえば、次の SQLステートメントでは、テーブルからすべてのデータが削除され、続いてデータベースからテーブルが削除されます。

DROP FROM users;

ユーザーは、データベースを変更するこのコマンドの出力を見る必要はありません。このような攻撃を防止するには、データベースのアクセス許可を読み取り専用に変更し、特定の書き込み操作のみを許可します。詳細については、データベースのドキュメントを参照してください。

ユーザーパスの指定

アプリケーションの安全性を確保し、ユーザーによる重要なデータへのアクセスを防ぐ方法には、指定された順序でページにアクセスするようにして、クライアントが認証メカニズムを回避できないようにすることが含まれます。リクエスト属性に値を設定するクラスを使用し、JSP でその属性をチェックする場合は、必ず最初のページをリクエストしないと 2 番めのページをリクエストできないように設定できます。

次の例では、logcheck.jsp ページや logme.jsp ページが WEB-INF ディレクトリに保管されているので、ユーザーはそれらのページにアクセスできません。logme.jsp ページでは、isLoggedIn 属性が true に設定されています(より堅牢な実装では、なんらかの認証メカニズムを含みます)。ユーザーは直接このページにアクセスできないので、セッション値の設定は侵入者から保護されます。

次のコード例で、logme.jsp ファイルを示します。

<% session.setAttribute("isLoggedIn", "true"); %>
<HTML><BODY>
<A HREF="TargetResource.jsp">保護リソースに移動します</A>
... //html body
</BODY></HTML>

保護リソース (TargetResource.jsp) の一番上にあるコードには、isLoggedIn の値をチェックするファイルが含まれます。チェックに合格した場合は、ターゲットリソースが返されます。チェックに不合格した場合は、ページから例外が送信され、JRun はユーザーにエラーページを転送します。次のコードで、TargetResource.jsp ページを示します。

<%@ include file="/WEB-INF/logcheck.jsp" %>
<HTML><BODY>
おめでとうございます...あなたはログインしています。
</BODY></HTML>

logcheck.jsp ページは TargetResource.jsp ページに含まれ、isLoggedIn 属性をチェックして、ユーザーがログインしたかどうかを判断します。次のコードは、logcheck.jsp ファイルを示します。

<%
  String isLoggedIn = (String) session.getAttribute("isLoggedIn");
  if (isLoggedIn.equals("true")) {
  } else {
   throw new Exception("ページはアプリケーション内から呼び出されませんでした。"); 
  }
%>

ターゲットリソースには、include ディレクティブを持つ logcheck.jsp ファイルが含まれます。これには、include アクション (jsp:include) を使用しません。それは、このアクションによって、インクルードするファイルにリクエストが戻され、isLoggedIn チェックを実行した後で、保護されたページのコンテンツの表示を防ぐことができないからです。

サンプルサーブレットを表示するには、samples JRun サーバを起動し、ブラウザで http://localhost:8200/techniques を開きます。

リソースへの直接アクセスの防止

タグライブラリ、JSP ソースページ、クラス、およびその他の重要なデータを、Web アプリケーションの WEB-INF ディレクトリに保管します。クライアントは、このディレクトリのリソースに直接アクセスできません。

ServletContext オブジェクトの getResource メソッドや getResourceAsStream メソッドを使用すると、Web アプリケーションのコンポーネントからデータにアクセスできます。 「ユーザーパスの指定」 の例では、logcheck.jsp ページを WEB-INF ディレクトリに保管することによって、このテクニックを使用します。

フィルタを使用して、承認や認証の宣言レイヤーを Web アプリケーションに追加することもできます。フィルタ使用の詳細については、弟 7 章、「フィルタ」を参照してください。

エラーのキャッチ

コードから生成されるエラーはすべてキャッチできるようにしてください。エラー出力から、物理パス、プラットフォームの詳細、クラス名や変数名などの、Web アプリケーションが動作しているシステムに関する重要情報が明らかになることがあります。

コードからは、ランタイムエラーに加えてコンパイル時エラーも生成されることがあるので、JSP 作成時のエラーのキャッチは特に重要です。JSP をプリコンパイルするとコンパイル時エラーをキャッチできるので、これはよい習慣ですが、運用環境で使用する場合は、その必要はありません。

JSP の例外処理メカニズムの使用の詳細については、第 10 章を参照してください。

コメントの削除

コメントにはしばしば、重要な情報を開発者に説明する実装の詳細が書かれています。悪意のあるユーザーは、システムへの攻撃に使用する情報をコメントから収集することがあります。

適切なレベルのコメントを使用するか、JSP や HTML ページからコメントをすべて削除してから、アプリケーションを運用環境にデプロイします。

JSP で使用できるコメントには 2 種類あります。

また、次のようなスクリプトレットに挿入するコメントは、エンドユーザーには見えません。

<%
  /* 次のステートメントで、ユーザーテーブルから userID を取得します */
  String sqlstmt = "select userID from users where lastname = 
request.getParameter("lastname")"; 
%>

価値あるログエントリの作成

ServletContext では log メソッドを利用できるので、Web アプリケーションに有用なログエントリを生成できます。これは、クライアントが未認可のリソースにアクセスを試みているかどうかを判断するのに役立ちます。JRun は、log メソッドの出力を /<JRun のルートディレクトリ>/servers/<サーバ名>/logs/<サーバ名>-event.log ファイルに書き出します。

コンテキストオブジェクトの log メソッドを使用するには、次の例で示されているように、サーバの /<JRun のルートディレクトリ>/servers/<サーバ名>/SERVER-INF/jrun.xml ファイルでデバッグレベルのロギングを有効にする必要があります。

<attribute name="debugEnabled">true</attribute>

使用可能なメソッドを使用して、リクエストしているクライアントの IP アドレス、Referer ヘッダー、クエリ文字列などのエントリをログに書き込みます。次のコード例で、価値あるログエントリの生成に有用なメソッドを示します。

...
ServletContext sc = this.getServletContext();
String[] info = new String[12];
info[0] = sc.getServletContextName();
info[1] = request.getMethod();
info[2] = getServletName();
info[3] = request.getContextPath();
info[4] = request.getRequestURI();
info[5] = request.getHeader("referer");
info[6] = request.getQueryString();
StringBuffer sb = request.getRequestURL();
info[7] = sb.toString();
info[8] = request.getHeader("Authorization");
info[9] = request.getRemoteAddr();
info[10] = request.getRemoteHost();
info[11] = request.getHeader("User-Agent");
sc.log("--------------------NEW REQUEST--------------------");
for (int i=0; i<info.length; i++) {
  sc.log(info[i]);
  out.println(info[i] + "<BR>");
}
...

このコード例では、次の出力に類似したログエントリが作成されます。

------------------------NEW REQUEST---------------------------
02/28 10:43:51 debug JRun Default Web Application
02/28 10:43:51 debug GET
02/28 10:43:51 debug TestLog
02/28 10:43:51 debug 
02/28 10:43:51 debug /servlet/TestLog
02/28 10:43:51 debug http://localhost:8100/JumpToTestLog.jsp
02/28 10:43:51 debug color=red&name=nick
02/28 10:43:51 debug http://127.0.0.1:8100/servlet/TestLog
02/28 10:43:51 debug 
02/28 10:43:51 debug 127.0.0.1
02/28 10:43:51 debug 127.0.0.1

サンプルサーブレットを表示するには、samples JRun サーバを起動し、ブラウザで http://localhost:8200/techniques を開きます。

ロギングをフィルタとして実装することによって、すべての Web コンポーネントでログエントリを標準化できます。またこうすることで、ロギングメカニズムは、サーブレットと JSP に対して透過性を持つことができるようになります。フィルタ作成の詳細については、弟 7 章、「フィルタ」を参照してください。

ロギングのレベルを上げるとコストがかさみます。ServletContext.log() への呼び出しでは、いずれも少量の処理リソースが使用されます。JRun のロギング実装方式では、ロギングの作業はログサービスに渡されますが、ここでもシステムリソースが少し消費されます。

ログファイルエントリの形式作成の詳細については、JMC のオンラインヘルプを参照してください。独自のロギングサービス設定の詳細については、『JRun 管理者ガイド』を参照してください。