ClamAVによるアップロードファイルのウィルスチェック
とあるRailsアプリケーションでアップロードされたファイルをウィルスチェックしたいというのがあって、実装したのでメモ。
方針
ClamAVというオープンソースのアンチウィルスエンジンがあるのでこれを使う。各種言語にクライアントライブラリ(Rubyはclamav-clientというgem)があったりして便利。
このアプリケーションはDockerで動かしているので、ClamAVのコンテナを立ててデーモンを動かし、ファイルアップロード時にアップロードされたファイルをClamAVのデーモンに渡してウィルス判定されたらバリデーションエラーになるようにする。
また、ファイルアップロードには shrine という gem を使っている。
実装
環境
docker-compose.yml:
--- version: '3.5' services: clamav: image: mkodockx/docker-clamav ports: - 3310 volumes: - clamav:/var/lib/clamav - ./docker/clamav/clamd.conf:/etc/clamd/clamd.conf:ro - ./docker/clamav/freshclam.conf:/etc/clamd/freshclam.conf:ro app: build: context: . dockerfile: docker/app/Dockerfile target: base command: > bash -c " rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" depends_on: - clamav volumes: - .:/app ports: - ${DOCKER_HOST_APP_PORT:-3000}:3000 environment: CLAMD_TCP_HOST: clamav CLAMD_TCP_PORT: 3310 volumes: clamav: driver: local
clamd.conf:
# Daemon Foreground true LocalSocket /var/run/clamav/clamd.ctl FixStaleSocket true LocalSocketGroup clamav LocalSocketMode 666 TCPSocket 3310 # Basic User clamav ScanMail true ScanArchive true ArchiveBlockEncrypted false MaxDirectoryRecursion 15 FollowDirectorySymlinks false FollowFileSymlinks false ReadTimeout 180 MaxThreads 12 MaxConnectionQueueLength 15 # Log LogFile /dev/stdout LogSyslog false LogRotate false LogFacility LOG_LOCAL6 LogClean false LogVerbose false LogTime true LogFileUnlock false LogFileMaxSize 0 # Detail PreludeEnable no PreludeAnalyzerName ClamAV DatabaseDirectory /var/lib/clamav OfficialDatabaseOnly false SelfCheck 3600 Debug false ScanPE true MaxEmbeddedPE 10M ScanOLE2 true ScanPDF true ScanHTML true MaxHTMLNormalize 10M MaxHTMLNoTags 2M MaxScriptNormalize 5M MaxZipTypeRcg 1M ScanSWF true DetectBrokenExecutables false ExitOnOOM false LeaveTemporaryFiles false AlgorithmicDetection true ScanELF true IdleTimeout 30 CrossFilesystems true PhishingSignatures true PhishingScanURLs true PhishingAlwaysBlockSSLMismatch false PhishingAlwaysBlockCloak false PartitionIntersection false DetectPUA false ScanPartialMessages false HeuristicScanPrecedence false StructuredDataDetection false CommandReadTimeout 5 SendBufTimeout 200 MaxQueue 100 ExtendedDetectionInfo true OLE2BlockMacros false ScanOnAccess false AllowAllMatchScan true ForceToDisk false DisableCertCheck false DisableCache false MaxScanSize 100M MaxFileSize 25M MaxRecursion 16 MaxFiles 10000 MaxPartitions 50 MaxIconsPE 100 PCREMatchLimit 10000 PCRERecMatchLimit 5000 PCREMaxFileSize 25M ScanXMLDOCS true ScanHWP3 true MaxRecHWP3 16 StreamMaxLength 25M Bytecode true BytecodeSecurity TrustSigned BytecodeTimeout 60000
freshclam.conf:
# Daemon Foreground true # Basic DatabaseOwner clamav Debug false MaxAttempts 5 DatabaseDirectory /var/lib/clamav DNSDatabaseInfo current.cvd.clamav.net ConnectTimeout 30 ReceiveTimeout 30 TestDatabases yes ScriptedUpdates yes CompressLocalDatabase no SafeBrowsing false Bytecode true NotifyClamd /etc/clamav/clamd.conf # Log UpdateLogFile /dev/stdout LogVerbose false LogSyslog false LogFacility LOG_LOCAL6 LogFileMaxSize 0 LogRotate false LogTime true # Check Checks 24 DatabaseMirror db.local.clamav.net DatabaseMirror database.clamav.net
Rails
Gemfile:
... # Upload file gem 'shrine' # Antivirus gem 'clamav-client', require: 'clamav/client'
config/initializers/clamav.rb:
class << Rails.application def clamav Thread.current['clamav'] ||= ClamAV::Client.new( ClamAV::Connection.new( socket: TCPSocket.new(Settings.clamav.tcp_host, Settings.clamav.tcp_port), wrapper: ClamAV::Wrappers::NewLineWrapper.new ) ) end end
app/uploaders/basic_uploader.rb:
class BasicUploader < Shrine plugin :validation_helpers Attacher.validate do result = Rails.application.clamav.execute(ClamAV::Commands::InstreamCommand.new(get)) errors << :has_virus if result.virus_name.present? end end
app/models/hoge_model.rb:
class HogeModel < ApplicationRecord include BasicUploader::Attachment.new(:file) end
テスト
EICARがウィルスチェック用の無害なテストウィルスを配布しているのでこれを使う。
$ docker-compose exec app rails c irb(main):001:0> model = HogeModel.new(file: Rails.root.join('fixtures/antivirus/eicar.com').open) irb(main):002:0> model.valid? irb(main):003:0> model.errors.details {:file=>[{:error=>:has_virus}]} irb(main):004:0> model = HogeModel.new(file: Rails.root.join('REDME.md').open) irb(main):005:0> model.valid? irb(main):006:0> model.errors.details {}