はじめに
この記事では、Java(JEEアプリケーション)でファイルのアンチウイルススキャンを実装する方法の一例を紹介します。現在のコンピュータ環境、特にWindowsオペレーティングシステムでは、ウイルスやトロイの木馬をはじめとするさまざまなマルウェアやスパイウェアが大きな問題となっています。
Javaで開発するアプリケーションに、外部からファイルをアップロードできるようにするという要件がある場合、セキュリティリスクが生じる可能性があります。ここで言うアップロードとは、HTTPプロトコルなり他の方法なりを使って、何らかの形で企業のファイアウォール内に外部のファイルを送れるようにすることを指します。こうした要件はエンタープライズアプリケーションでは非常によくあるもので、しかもJavaは最も広く使われているWeb開発プラットフォームの1つです。にもかかわらず、このように大きく口を開けたセキュリティリスクが往々にして見過ごされているのは、残念なことです。
Java Development Kit(JDK)には、アンチウイルススキャンのための機能は標準装備されていません。その理由として大きいのは、Javaはプログラム言語であり、ウイルススキャンのパッケージを備えていないということです。しかも、アンチウイルスソフトウェアはSunの専門外ですし、同社のビジネスモデルにも含まれていません。ウイルス対策のソフトウェア(あるいはJavaパッケージ)を開発することや、さらに重要な、維持やメンテナンスを続けていくことは、Sunにとってかなりの重荷です。ウイルスは常に進化を続けていくので、ウイルス定義を最新の状態に維持していくのは骨の折れる作業です。ウイルスの検出と対処を行うための製品は、McAfee、Symantec、Zone Labsなどの大企業が開発し、多大なリソースを投入してそのメンテナンスにあたっています。
アプリケーション環境
Javaでファイルのウイルススキャンを実装するには、サードパーティ製のパッケージを利用する必要があります。この記事では、Java APIを備えたSymantec Scan Engine(SSE)パッケージを使うことにします。このパッケージは、TCP/IPサーバーとして動作するアプリケーションです。プログラミングインターフェイスが用意されており、コンテンツのスキャン技術をJavaアプリケーションに組み込むことができます。この記事ではSymantec Scan Engine 5.1を使用します。この製品には、UNIX版とWindows版の両方があります。
他社製のアンチウイルスパッケージを使用する場合は、どんなAPIが用意されているかは各自で調べてください。もっとも、基本的なアプローチは似ているはずです。また、ここで紹介する実装方法は、使用するJEEテクノロジは問わず、最近のMVCフレームワーク(StrutsやSpringなど)に対応しています。
アーキテクチャは次のようになります。まず、SSEを常時稼動するサーバーマシンが必要です。アプリケーションサーバーをホストするマシンと同じにすることもできますが、エンタープライズ環境の場合は、別のマシンにすることをお勧めします。また、ファイアウォールでデフォルトポートを開けておき、スキャンエンジンと通信できるようにしておく必要があります。ファイルスキャンを行う必要があるすべてのJEEアプリケーションは、デフォルトポートを通じてSSEサーバーマシンと通信できます。なお、別々のアプリケーションサーバーで稼動している複数のアプリケーションが同じスキャンサーバーを共用することもできます。詳細については、SymantecのWebサイトにある、Symantec Scan Engine(SSE)のインストールガイドを参照してください。
スキャンが必要な外部ファイルを、プログラミングインターフェイス(デフォルトポートを使用するJava API)を使用してSSEに送信すると、そのファイルに対する他の操作を行う前に、SSEから結果コードが戻ります。たとえば、Webメール型のアプリケーションで、外部ユーザーが添付ファイルとしてファイルをアップロードしたとします。この場合、アプリケーションは、SSEのAPIを呼び出し、OKかNGかを示すリターンコードに基づいて、アップロードの結果を判断し、その電子メールを実際に送信してよいかどうかを決定します。Yahooメールのアカウントをお持ちの方なら、Norton Antivirusですべての添付ファイルがスキャンされるのを見たことがあるかもしれません(Javaを使った実装ではありませんが)。
サーバーへのScan Engineのインストールの詳細については、Symantecのサイトにある、Symantec Scan Engine(SSE)実装ガイドを参照してください。
SSEについて頭に入れておくべきポイントを以下にまとめます。
- SSEをインストールする前に、サーバーにJava 2 SE Runtime(JRE)5.0 Update 6.0以降をインストールする必要があります。
- インストール後に、Symantec Scan Engineデーモンが稼動していることを確認してください。UNIX版の場合、UNIXのコマンドプロンプトで次のコマンドを入力します。
次のようなプロセスのリストが表示されます。
- 5358 ? 0:00 symscan
- 5359 ? 0:00 symscan
何も表示されない場合、SSEプロセスは起動していません。
SSEプロセスが起動しなかった場合、次のコマンドを入力してSSEを再起動します。
/etc/init.d/symscan restart
- 常に最新のウイルス定義を適用しておくことが最も重要な責務です。最新のデータがインストールされていないと、スキャン全体の実効性がなくなります。Symantec製品では、LiveUpdate機能によって、最新のファイル定義が自動的にダウンロードされます。ホストサーバーがSymantecの更新サービスに接続できるようにファイアウォールのルールを設定しておいてください。
プロジェクトのセットアップ
この記事のために、Symantec SSE APIのラッパーとなる「av.jar」を用意しました。SymantecのJava APIを使用し、SSEサーバーのクライアントとして機能して、サーバーとのすべての通信を担います。「av.jar」は、本稿の冒頭のリンクからダウンロードできます。SSEに対して「av.jar」を使用するには、これをJava CLASSPATHに含める必要があります。このjarにはAVClientというクラスが含まれています。これは、SSEに対してバイト列でファイルを送信し、結果を返すという処理そのものを行うクラスです。
私のプロジェクト設定では、System.getPropertyを通じてアクセスする3つの変数を追加しています。次のような変数です。
AV_SERVER_HOST=192.168.1.150
AV_SERVER_PORT=''1344''
AV_SERVER_MODE=SCAN
AV_SERVER_HOSTはScan Engineがインストールされているマシンのホスト名またはIPです。
AV_SERVER_PORTはScan Engineが受け取るファイルをリッスンするポートです。
AV_SERVER_MODEはスキャンモードで、次のいずれかです。
- NOSCAN ― スキャンを行いません(先頭が"SCAN"以外のどのキーワードを指定しても、Scan Engineの呼び出しは無視され、ファイルの転送とスキャンは行われません)。
- SCAN ― ファイルまたはバイトストリームをスキャンしますが、感染の修復は行いません。
- SCANREPAIR ― ファイルをスキャンし、感染の修復を試みますが、それ以外の処理は行いません。
- SCANREPAIRDELETE ― ファイルをスキャンし、感染の修復を試みます。修復できないファイルは削除します。
注
ファイルストリーム(バイト列)のスキャンの場合、有効な値はSCANとNOSCANのみです。
SSEでスキャンを行うJava APIの使用
スキャンを行うクラスでは、「av.jar」内のAVClientオブジェクトに備わっているスキャンAPIを呼び出します。AVClientオブジェクトはScan Engineサーバーへの接続を確立し、以下のAPIを含みます。
図2 Scan Engineサーバーと通信するための主なAPI
スキャンの対象がファイルシステム上のファイルで、SCANモード(スキャンのみのモード)を使用する場合は、パラメータとしてファイル名のみを受け取るメソッドを呼び出します。
スキャンの対象がファイルシステム上のファイルで、SCANREPAIRモードまたはSCANREPAIRDELETEモードを使用する場合は、入力ファイル名と出力ファイル名を受け取るメソッドを呼び出します。
スキャンの対象がメモリ内のファイル(バイト列)の場合は、バイト列を受け取るメソッドを呼び出します。
サンプルコードを以下に示します。
設定パラメータを初期化します。
static String avMode =
(System.getProperty("AV_SERVER_MODE") != null)
? (String) System.getProperty("AV_SERVER_MODE") : "NOSCAN";
static boolean scan = avMode.startsWith("SCAN");
static String avServer =
(String) System.getProperty("AV_SERVER_HOST");
static int avPort =
Integer.parseInt( (String) System.getProperty("AV_SERVER_PORT"));
メモリ内ファイルのバイト列をスキャンしてチェックする例です。
public void scanFile(byte[] fileBytes, String fileName)
throws IOException, Exception {
if (scan) {
AVClient avc = new AVClient(avServer, avPort, avMode);
if (avc.scanfile(fileName, fileBytes) == -1) {
throw new VirusException("WARNING: A virus was detected in
your attachment: " + fileName + "<br>Please scan
your system with the latest antivirus software with
updated virus definitions and try again.");
}
}
}
MVCハンドラ内でこのコードを使用する場合は、カスタムのVirusExceptionをスローでき、呼び出し側のメソッドでこれをチェックして必要なクリーンアップ処理を実行できます。このクラスも「av.jar」に含まれています。
使用例を次に示します。
catch (Exception ex) {
logger.error(ex);
if (ex instanceof VirusException) {
// do something here
}
else {
// there was some other error - handle it
}
}
Scan EngineのクライアントAPIの詳細については、Symantec Scan Engineソフトウェア開発者ガイドを参照してください。
SSEの応答コードと通知オプション
SSEに接続できなかった場合や、SSEライセンスが無効になったり有効期限が切れたりした場合に備えて、Javaコードには、システムサポート担当者に送信する通知や電子メールを自動生成するためのフックが必要です。こうした通知を生成する機能を使用するためには、アンチウイルスのクライアントコードをアプリケーションに組み込むときに、javamailやその他の電子メールパッケージを使用することが必要な場合があります。AVClientオブジェクトのconnect (String host) API、およびヘルパークラスAVRespondのrequest(String server_response)メソッドを参照してください。
コードの一覧を以下に示します。
- private String one = "ICAP/1.0 100";
- private String two = "ICAP/1.0 200";
- private String three = "ICAP/1.0 201";
- private String four = "ICAP/1.0 204";
- private String five = "ICAP/1.0 400";
- private String six = "ICAP/1.0 403";
- private String seven = "ICAP/1.0 404";
- private String eight = "ICAP/1.0 405";
- private String nine = "ICAP/1.0 408";
- private String ten = "ICAP/1.0 500";
- private String eleven = "ICAP/1.0 503";
- private String twelve = "ICAP/1.0 505";
- private String thirteen = "ICAP/1.0 533";
- private String fourteen = "ICAP/1.0 539";
- private String fifteen = "ICAP/1.0 551";
- private String sixteen = "ICAP/1.0 558";
私は、SSEサーバーからの応答文字列に、ライセンスの有効期限切れに対応する文字列「ICAP/1.0 558」を追加しました。対応が必要な主なコードには、ウイルスを示すtwoとthree、およびファイルが異常なしであることを示すfourがあります。
応答コードの定義の一覧については、Symantec Scan Engineソフトウェア開発者ガイド(表3-4の状態コード)を参照してください。
ライセンスが無効の場合に管理者に電子メールを送信するには、次のようにします。
if (server_response.equals(fourteen) ||
server_response.equals(sixteen)) {
// license is bad
// if you want to let files through without actually scanning
the_return = "''clean''";
// send email - this must be working if option is "clean"
String body = "Note the license for Symantec Scan Engine is
invalid or has expired!¥n¥rPlease see - class
AVRespond for more info.¥n¥rThis email is sent
every time a client uploads an un-scanned file";
try {
sendMail("symantec_scan_engine@mycompany.com",
"YOU@mycompany.com", "Invalid or expired license for
Symantec Scan Engine", body);
}
catch (Exception ex) {
logger.error("AV client could not send email notification¥nMake
sure properties has ¥"mail.smtp.host¥" entry in
it ¥n " + ex);
}
}
まとめ
この記事では、JEEアプリケーションにファイルのアンチウイルススキャン機能を組み込む方法について説明しました。驚いたことに、このしくみについて知っている開発者はあまり多くありません。外部ファイルをスキャンせずにおくことはセキュリティリスクとなり得るにもかかわらずです。
現在のところ、スキャン処理の実装はサードパーティベンダごとに固有で、独自のJava APIを備えた製品を利用することになります。Java開発者としては、スキャンAPIの標準プロトコルをSunが策定して、各ベンダに準拠してもらう形になった方がうれしいと思います。ちょうど、Java Messaging Service(JMS)プロトコルの場合と同じようにです。エンタープライズ分野におけるスキャン処理は、JMSほどの存在感はないにせよ、一部のエンタープライズアプリケーションにとって重要な側面であることは間違いありません。
関連情報
ウォールストリート有数の企業でエンタープライズレベルのプロジェクトに携わる。また、米国政府の防衛関連の仕事も手がけている。特に関心を寄せているのは、オブジェクト指向プログラミング方法論、UI、デザインパターン。