Skip to content

添付ファイルのウイルススキャン ​

第1版作成 最終更新 (日本時間)
確認バージョン1.5.1.01.5.8.1

本体の標準機能ではありません

プリザンター 1.5.8.1 にはアップロードされたファイルの中身を検査する仕組みがありません。このページは組み込む場合の設計メモです。ClamAV を使う実装コード(IVirusScanService などの全文)と clamd のセットアップは添付ファイルのレシピの「ウイルススキャンを追加する(本体改変)」にあります。

前提にした現行実装 ​

アップロード時の検査は次のものだけです(1.5.8.1)。

検査場所内容
件数・サイズBinaryValidators.OnUploading()項目の LimitQuantity・LimitSize・TotalLimitSize、ローカル保存時の LocalFolderLimitSize・LocalFolderTotalLimitSize、テナントの StorageSize
ファイル名・拡張子BinaryValidators.OnValidatingFormUpload()IsValidFileName()(NUL・..・/・\・:・255 文字超を拒否)と IsAllowedExtension()(Form.json の AttachmentExcludedExtensions)
ハッシュBinaryUtilities.ValidateFileHash()送られたハッシュと保存したファイルの一致確認(改ざん・欠損の検出で、マルウェア検出ではない)
画像画像アップロード時ImageSharp で画像として読めるか

OnValidatingFormUpload() は先頭で if (!context.IsForm) return Error.Types.None; としているので、ファイル名と拡張子の検査は公開フォームからのアップロードだけに掛かります(BinaryValidators.cs#L636-L689)。拡張子の検査の詳細は添付ファイルの拡張子制限を参照してください。

どちらにしても、.docx・.pdf・.zip のように許可された拡張子に偽装したファイルや、マクロ付きの Office 文書は通ります。

スキャンエンジンの比較 ​

手法WindowsLinux無償呼び出し方向き・不向き
ClamAV(clamd デーモン)可可可(GPL v2)TCP / Unix ソケットでストリーム送信。.NET は nClam(MIT)常駐するので速い。公式 Docker イメージ clamav/clamav がある
ClamAV(clamscan コマンド)可可可プロセス起動毎回エンジンを初期化するため 1 ファイル数秒〜十数秒かかる。高頻度には不向き(clamdscan なら clamd 経由で速い)
Windows Defender(AMSI)可不可可(OS 標準)amsi.dll を P/Invoke(AmsiScanBuffer)追加ソフト不要。AMSI 対応のサードパーティ製 AV が入っていればそのエンジンが使われる
Windows Defender(MpCmdRun.exe)可不可可-Scan -ScanType 3 -File をプロセス起動。終了コード 2 が検出手軽だがプロセス起動のコストがある
YARA可可可(BSD)ルールでパターンマッチ(.NET は dnYara)AV の代わりではなく補完。ルールの保守に専門知識が要る
VirusTotal API可可無償枠は 1 日 500 件・1 分 4 件REST API70 以上のエンジンで判定できるが、アップロードしたファイルは共有されるので機密文書には使えない。SHA-256 のハッシュだけを照会する使い方なら本体は送らない

Windows と Linux の両方で動かすなら ClamAV(clamd)が現実的な第一候補です。Windows だけの環境なら AMSI が追加ソフトなしで済みます。OS を判別して Windows は AMSI、Linux は ClamAV に振り分ける構成も取れますが、プラットフォームごとのコードを保守することになります。

組み込み方の比較 ​

方式本体改修確実さバージョンアップの影響内容
A. 本体コードに直接入れる要高い大きいBinaryUtilities.UploadFile() などにスキャンを差し込む。全経路を確実に押さえられる
B. プラグインにする要中中ExtendedLibraries のプラグイン機構に拡張点を足す。ただし 1.5.8.1 の PluginTypes は Pdf だけで(ExtendedPlugin.cs#L10-L13)、スキャン用の拡張点とアップロード処理からの呼び出しを本体に足す必要がある
C. リバースプロキシでスキャン不要中なしnginx + ModSecurity などでマルチパートのファイル部分を ClamAV に回す。マルチパートの解析とチャンクアップロードへの対応が難しい
D. サーバースクリプトから呼ぶ不要低いなしBeforeCreate・BeforeUpdate で外部のスキャン API を呼ぶ。一時ファイルの実体やバイナリにスクリプトから届くかが課題

本体を触らずに済むのは C、確実さを取るなら A です。以下は A の設計です。

本体に組み込む場合の設計 ​

設定:ScanMode で対象を切り替える ​

設定は App_Data/Parameters/VirusScan.json に置き、ScanMode でスキャンする経路を選びます。既定は Disabled で、ファイルを置かなければ(Read<VirusScan>(required: false) で null)スキャンしません。項目の一覧は添付ファイルのレシピにあります。

アップロード経路AllFormOnlyDisabled
BinariesController.Upload(認証ユーザーの添付ファイル)スキャン--
BinariesController.UploadImage(認証ユーザーの画像)スキャン--
FormBinariesController.Upload(公開フォームの添付ファイル)スキャンスキャン-
FormBinariesController.UploadImage(公開フォームの画像)スキャンスキャン-
Api/BinariesController.Upload(API)スキャン--
使い方モード
社内だけで使うDisabled
公開フォームがあり、社内ユーザーは信頼するFormOnly
公開フォームと API を不特定多数に開いているAll
ClamAV サーバーの資源が限られるFormOnly

改修するファイル ​

区分ファイル内容
新規Implem.ParameterAccessor/Parts/VirusScan.csパラメータクラス
新規App_Data/Parameters/VirusScan.json既定値(ScanMode: "Disabled")
新規Libraries/Security/IVirusScanService.cs ほか 3 つインターフェース・ClamAV 実装・無効時の実装・結果クラス
新規App_Data/Displays/VirusDetected.json・VirusScanError.jsonエラーメッセージ
変更Implem.ParameterAccessor/Parameters.cspublic static VirusScan VirusScan;
変更Implem.DefinitionAccessor/Initializer.csSetParameters() に Read<VirusScan>(required: false)
変更Startup.csConfigureServices で DI 登録
変更Implem.Pleasanter.csprojnClam の参照
変更Models/Binaries/BinaryUtilities.csスキャンの呼び出し
変更Controllers/Api/BinariesController.csAPI 経路のスキャン
自動生成Libraries/General/Error.cs・Libraries/Responses/Messages.csDisplays を足して CodeDefiner を実行すると再生成される

Parameters は Startup のコンストラクタで Initializer.Initialize() が読み込むので、ConfigureServices の時点で参照できます。

挿入箇所 ​

BinaryUtilities は static クラスなので DI を直接受けられません。AspNetCoreHttpContext.Current.RequestServices.GetService<IVirusScanService>() でサービスを取り出すヘルパーを用意し、見つからなければ無効時の実装を返すようにします。呼び出し元がフォームかどうかは引数(isFormUpload)で渡します。

添付ファイル(UploadFile):1.5.8.1 では、OnValidatingFormUpload() の検査の後、一時保存先(TemporaryBinaryStorageProvider)で Rds とファイルシステムに分岐する直前に入れます(BinaryUtilities.cs#L823-L896)。

図を読み込み中…

保存前にスキャンする場合、PostedFile のストリームを MemoryStream に写してからスキャンし、保存処理にはコピーしたバイト列を渡すかストリームを巻き戻します(入力ストリームは一方向なので、読み切ると保存できなくなります)。保存後にスキャンするなら、ファイルシステム保存では保存したファイルを読み直し、検出したら削除します。

画像(UploadImage):1.5.8.1 では実際の保存を行うオーバーロードがバイト列(byte[] bin)を受け取るので、その先頭でスキャンできます(BinaryUtilities.cs#L509-L514)。画像を対象にするかは ScanImages で切り替えます。

API(Api/BinariesController.Upload):一時ファイルを App_Data/Temp に保存して ValidateFileHash を通した後、CreateAttachment の前でファイルを読んでスキャンし、検出したら一時ファイルを削除してエラーを返します(Api/BinariesController.cs#L95-L250)。API は isFormUpload = false なので All のときだけスキャンします。

チャンクアップロード ​

Content-Range で分割して送られた場合、途中のチャンクだけではスキャンできません。ファイル全体が揃った時点(contentRange == null || contentRange.To + 1 == contentRange.Length)でスキャンします。ファイルシステム保存では追記で完成したファイルを、Rds 保存では完成した Bin を対象にします。分割してシグネチャ検出を逃れる攻撃もこれで防げます。

エラーメッセージ ​

Error.Types と Messages は CodeDefiner が App_Data/Displays/*.json から生成するので、直接編集しません。Displays の JSON を足して CodeDefiner を実行します。

json
{
    "Id": "VirusDetected",
    "Type": 240,
    "Languages": [
        {
            "Body": "A virus was detected in the uploaded file: \"{0}\". The upload has been rejected."
        },
        {
            "Language": "ja",
            "Body": "アップロードされたファイルからウイルスが検出されました: 「{0}」。アップロードは拒否されました。"
        }
    ]
}

Type は本体のエラーメッセージ(OverLimitApi.json・RateLimitExceeded.json など)と同じ 240 にします。スキャン自体の失敗用に VirusScanError.json も同じ形で作ります。Displays が残っていれば、バージョンアップ後も CodeDefiner の再実行で反映されます。

ログ ​

検出とスキャンエラーは SysLogModel に Warning で記録します(method に VirusScan.Detected / VirusScan.Error、message にウイルス名とファイル名)。ウイルス名やエラー文言はそのまま出すとログインジェクションの余地があるので整形してから書きます。

画面の表示 ​

添付ファイルのアップロードは、ファイルごとのプログレスバー(attachments.js)で進捗を出しています(#LoaderContainer の全画面ローダーはレコード保存などの Ajax 用で、アップロード中は出ません)。スキャンはサーバー側で同期的に終わってから応答が返るので、ポーリングは要りません。表示の案は次のとおりです。

案内容評価
プログレスバーを「スキャン中」にする送信が 100% になったらバーの色と文言を変え、応答を待つファイルごとの状態が分かる
全画面ローダー($p.loading())送信前に出して応答で消す実装は 1 行だが画面全体が止まる
バーの下に文言を出す「ウイルススキャン中...」とスピナーを出すバーの改修より独立して作れる

大きなファイルはスキャンに時間がかかるので、TimeoutMs とクライアント側の Ajax のタイムアウトを揃えます。

運用上のリスク ​

リスク内容対策
clamd との通信の改ざんclamd の TCP 通信は暗号化されないclamd を localhost か専用ネットワークに置く(TCPAddr 127.0.0.1)
サービス拒否大きなファイルの連続アップロードで clamd を枯渇させるMaxFileSizeMB、clamd の接続数上限、アップロードのレート制限(レート制限。公開フォームは PublicForm ポリシー)
圧縮爆弾展開すると巨大になるアーカイブclamd.conf の MaxScanSize・MaxFileSize・MaxRecursion・MaxFiles
タイムアウトによるすり抜けRejectOnScanError: false だとスキャンが失敗したファイルが通る本番は RejectOnScanError: true
定義ファイルの陳腐化定義が古いと新しいマルウェアを見逃すfreshclam の自動更新と監視
clamd 自体の脆弱性既知の CVE が踏み台になる定期的な更新
過信「スキャン済み」で他の対策を怠る拡張子制限・サイズ制限と組み合わせる多層防御の一部と位置付ける

動作確認には EICAR テストファイルを使います。.docx などに名前を変えても検出されるかで、拡張子偽装への耐性も確かめられます。確認すべき組み合わせは、ScanMode 3 種 × 経路 3 種、clamd 停止時の RejectOnScanError の両方、サイズ超過、画像の ScanImages の両方、チャンクアップロード、ログ出力です。

関連ページ ​

変更履歴

第1版レートリミッターの解説と、フォーム投稿の制限・ウイルススキャン・拡張子制限・外部検索エンジン・管理画面設定の改修・設計メモを追加