Bir proje geliştirirken dosya yükleme özelliği eklemek genelde en "rutin" işlerden biri gibi görünür. Bir MultipartFile alırız, bir klasöre veya S3 gibi bir storage servislerine göndeririz ve biter. Hatta bazen işi sağlama almak için uzantı ve MIME gibi basit kontroller ekleyip arkamıza yaslanırız.
Sistemimize kabul ettiğimiz her dosya, aslında sunucumuzun çalışma alanına doğrudan dahil ettiğimiz kaynağı belirsiz bir veri paketidir. Peki, bu paketin içinde çalışma zamanında tetiklenecek bir script, bir shell veya sisteminizi manipüle edecek bir trojan olup olmadığını gerçekten biliyor musunuz?
"Kullanıcılarımın hepsi şirket çalışanı, o yüzden güvenilir" veya "Ben zaten sadece resim formatı kabul ediyorum" demek, günümüz siber saldırı dünyasında maalesef teknik karşılığı olmayan bir tesellidir. Saldırganlar artık dosyaların içine zararlı payload'lar gizleyebiliyor, dosya imzalarını (magic bytes) manipüle ederek en sıkı Content-Type kontrollerini bile bypass edebiliyor. Gerçek bir güvenlik katmanı, dosyanın sadece etiketine bakmak yerine, o dosya daha sisteme yazılmadan içeriğini derinlemesine analiz etmekle başlıyor.

Bu yazıda, Spring Boot ile geliştirdiğim production-ready şablon olan Kavun projesine ClamAV entegrasyonunu nasıl eklediğimi anlatacağım. ClamAV nedir, nasıl çalışır, Spring Boot'a nasıl entegre edilir adım adım gideceğiz.
ClamAV Nedir?
ClamAV, açık kaynaklı bir antivirüs motorudur. 2001'den beri aktif olarak geliştirilen bu motor, özellikle sunucu taraflı kullanım için tasarlanmıştır. Masaüstü antivirüsler gibi arka planda sürekli çalışmaz; sen dosyayı verirsin, o tarar ve sonucu döner.
Temel özellikleri:
- Açık kaynak ve ücretsiz (GPL lisansı)
- 8 milyonun üzerinde zararlı yazılım imzası
- PDF, Office dosyaları, arşivler dahil geniş format desteği
- Daemon modu:
clamdservisi olarak çalışır, TCP/Unix socket üzerinden sorgu alır - Düzenli imza güncellemeleri (
freshclam)
Neden Dosya Yüklemelerini Taramalısın?
Bir upload endpoint'in varsa şu saldırı vektörleri gerçektir:
- Polyglot dosyalar: Hem geçerli bir JPEG hem de çalıştırılabilir bir script olan dosyalar. Uzantı ve MIME kontrolünden geçer ama zararlıdır.
- Arşiv bombaları: Açıldığında terabaytlarca yer kaplayan sıkıştırılmış dosyalar. Sunucunu doldurur.
- Makro içeren Office dosyaları: .docx veya .xlsx içine gömülü zararlı makrolar.
- Web shell'ler: Sunucuda komut çalıştırma yetkisi veren gizlenmiş PHP/JSP dosyaları.
ClamAV bu tehditlerin tamamını imza tabanlı olarak tespit eder.
Nasıl Çalışıyor?

clamd ayrı bir Docker container'da çalışır. Spring Boot uygulaması ona TCP üzerinden bağlanır ve dosyayı stream olarak gönderir. Clamd tarar, OK ya da FOUND döner.
Docker ile ClamAV Kurulumu
docker-compose.yml dosyasına ClamAV servisini ekleyelim:
services:
clamav:
image: clamav/clamav:latest
container_name: kavun-clamav
restart: unless-stopped
ports:
- '3310:3310'
environment:
CLAMAV_NO_FRESHCLAM: "false"
CLAMD_STARTUP_TIMEOUT: 90
volumes:
- clamav-data:/var/lib/clamav
healthcheck:
test: ["CMD", "/usr/local/bin/clamdcheck.sh"]
interval: 60s
timeout: 10s
retries: 3
start_period: 300s
networks:
- kavun-network
clamav-data:
driver: localClamAV ilk başladığında imza veritabanını indirdiği için birkaç dakika beklemek gerekebilir. freshclam servisi bu imzaları düzenli olarak günceller.
Spring Boot Entegrasyonu
Kavun projesinde altyapı olarak Gradle kullanıyorum. İlk iş olarak, ClamAV ile TCP üzerinden konuşmamızı sağlayacak istemciyi (client) projeye dahil ederek başlıyoruz.
ext {
set('clamavClientVersion', '2.1.2')
}
dependencies {
implementation "xyz.capybara:clamav-client:${clamavClientVersion}"
}
# application.properties
# ===============================
# = FILE VIRUS SCAN CONFIGURATION
# ===============================
clamav.enabled=${CLAMAV_ENABLED:true}
clamav.host=${CLAMAV_HOST:localhost}
clamav.port=${CLAMAV_PORT:3310}
clamav.timeout=${CLAMAV_TIMEOUT:5000}
clamav.virus-detection-action=${CLAMAV_VIRUS_DETECTION_ACTION:REJECT}
clamav.async-scan=${CLAMAV_ASYNC_SCAN:false}
// ClamAVProperties.java
@Data
@Validated
@Configuration
@ConfigurationProperties(prefix = "clamav")
public class ClamAVProperties {
private boolean enabled;
private String host;
private int port;
private int timeout;
private boolean asyncScan;
}Clamd (ClamAV Daemon) ile iletişim kurarken protokol oldukça basittir: Sunucuya zINSTREAM\0 komutu gönderilir, ardından dosya 4 byte'lık küçük parçalar (chunk) halinde iletilir.
Ancak biz bu karmaşıklığı ClamAVService içinde soyutladık. Yazdığım bu servis, sadece bir tarama motoru değil; aynı zamanda sistemin ayakta olup olmadığını kontrol eden (isAvailable) ve versiyon takibi yapan bir yapıya sahip:
// ClamAVService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class ClamAVService {
private final ClamAVProperties clamAVProperties;
/**
* Dosya Taraması
*
* @param file Taranacak dosya
* @return Tarama ayrıntılarını içeren ScanResult
* @throws VirusDetectedException virüs tespit edilirse ve eylem REJECT ise
* @throws IOException dosyayı okurken bir hata varsa
*/
public ScanResult scanFile(MultipartFile file) throws IOException {
if (!clamAVProperties.isEnabled()) {
LOG.debug("ClamAV scanning is disabled. Skipping scan for file: {}", file.getOriginalFilename());
return null;
}
LOG.info("Scanning file for viruses: fileName={}, size={}", file.getOriginalFilename(), file.getSize());
try (InputStream inputStream = file.getInputStream()) {
ClamavClient client = new ClamavClient(clamAVProperties.getHost(), clamAVProperties.getPort());
ScanResult result = client.scan(inputStream);
if (result instanceof ScanResult.OK) {
LOG.info("File is clean: {}", file.getOriginalFilename());
return result;
} else if (result instanceof ScanResult.VirusFound virusFound) {
Map<String, Collection<String>> viruses = virusFound.getFoundViruses();
String virusSignature = viruses.keySet().toString();
LOG.warn("Virus detected in file: fileName={}, virus={}", file.getOriginalFilename(), virusSignature);
throw new VirusDetectedException(file.getOriginalFilename(), virusSignature);
} else {
LOG.error("Unexpected scan result type: {}", result.getClass().getName());
throw new RuntimeException("Unexpected scan result from ClamAV");
}
} catch (VirusDetectedException e) {
throw e;
} catch (Exception e) {
LOG.error("Error scanning file with ClamAV: fileName={}, error={}",
file.getOriginalFilename(), e.getMessage(), e);
if (clamAVProperties.isEnabled()) {
throw new RuntimeException(ClamAVConstants.VIRUS_SCANNER_NOT_AVAILABLE, e);
}
return null;
}
}
/**
* Bir dosyanın virüs içerip içermediğini kontrol eder.
*
* @param file Kontrol edilecek dosya
* @return Virüs tespit edilirse true, aksi takdirde false
*/
public boolean isVirusDetected(MultipartFile file) {
try {
ScanResult result = scanFile(file);
return result instanceof ScanResult.VirusFound;
} catch (VirusDetectedException e) {
return true;
} catch (Exception e) {
LOG.error("Error checking virus status: {}", e.getMessage(), e);
return false;
}
}
/**
* ClamAV hizmetinin kullanılabilir olup olmadığını ve yanıt verip vermediğini kontrol eder.
*
* @return ClamAV kullanılabilirse true, aksi takdirde false
*/
public boolean isAvailable() {
if (!clamAVProperties.isEnabled()) {
return false;
}
try {
ClamavClient client = new ClamavClient(clamAVProperties.getHost(), clamAVProperties.getPort());
client.ping();
LOG.debug("ClamAV service is available");
return true;
} catch (Exception e) {
LOG.warn("ClamAV service is not available: {}", e.getMessage());
return false;
}
}
/**
* ClamAV sürüm bilgilerini alır.
*
* @return Sürüm dizesi veya mevcut değilse null
*/
public String getVersion() {
if (!clamAVProperties.isEnabled()) {
return null;
}
try {
ClamavClient client = new ClamavClient(clamAVProperties.getHost(), clamAVProperties.getPort());
return client.version();
} catch (Exception e) {
LOG.warn("Failed to get ClamAV version: {}", e.getMessage());
return null;
}
}
}
@Getter
public class VirusDetectedException extends RuntimeException {
private final String fileName;
private final String virusSignature;
public VirusDetectedException(String fileName, String virusSignature) {
super(String.format("Virus detected in file '%s': %s", fileName, virusSignature));
this.fileName = fileName;
this.virusSignature = virusSignature;
}
public VirusDetectedException(String fileName) {
super(String.format("Virus detected in file '%s'", fileName));
this.fileName = fileName;
this.virusSignature = "Unknown";
}
}
@Transactional
public FileMetadata uploadFile(MultipartFile file, FileType fileType, EntityType entityType, UUID entityId) {
validateFile(file);
try {
// Virus Tarama
LOG.debug("Starting virus scan for file: {}", file.getOriginalFilename());
ScanResult scanResult = clamAVService.scanFile(file);
boolean isVirusDetected = scanResult instanceof ScanResult.VirusFound ? true : false;
UploadStatus initialStatus = UploadStatus.COMPLETED;
String errorMessage = null;
if (isVirusDetected) {
Object virusFound = scanResult;
String virusSignature = virusFound.toString();
errorMessage = "Virus detected: " + virusSignature;
LOG.warn("Virus detected in file: fileName={}, virus={}", file.getOriginalFilename(), virusSignature);
// File will not be uploaded
throw new VirusDetectedException(file.getOriginalFilename(), virusSignature);
} else {
LOG.debug("File passed virus scan: {}", file.getOriginalFilename());
}
// Object Key oluşturma
String objectKey = generateObjectKey(file.getOriginalFilename(), entityType, entityId);
// checksum hesaplama
String checksum = calculateChecksum(file);
// Yükleme
PutObjectResponse response = cephStorageService.uploadFile(objectKey, file);
// DB Metadata
FileMetadata metadata = createMetadataEntry(file, objectKey, checksum, fileType, entityType, entityId, objectKey, response.eTag());
metadata.setUploadStatus(initialStatus);
metadata.setErrorMessage(errorMessage);
metadata = fileMetadataService.save(metadata);
LOG.info("File uploaded successfully: objectKey={}, fileName={}, status={}, virusDetected={}", objectKey, file.getOriginalFilename(), initialStatus, isVirusDetected);
return metadata;
} catch (VirusDetectedException e) {
throw e;
} catch (Exception e) {
LOG.error("Failed to upload file: {}", e.getMessage(), e);
throw new InvalidServiceRequestException("Failed to upload file: " + e.getMessage());
}
}EICAR Test Dosyası
"Tamam kurduk ama gerçekten çalışıyor mu?" sorusunun cevabı EICAR test dosyasında gizli. Bu aslında gerçek bir virüs değil; sadece antivirüs yazılımlarının doğru yapılandırılıp yapılandırılmadığını anlamak için kullanılan standart bir metin dosyası.
İçinde sadece şu dize bulunan bir .txt dosyası oluşturup sisteminize yüklemeyi deneyin:
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*Eğer yazdığınız servis bu dosyayı yüklerken VirusDetectedException fırlatıyorsa, tebrikler; artık çok daha güvenli bir dosya yönetim sistemine sahipsiniz.
Dikkat Edilmesi Gereken Noktalar
- Fail-Safe mi, Fail-Fast mı? (ClamAV'a ulaşılamazsa ne yapmalı?)
- Fail-Fast (Güvenlik Öncelikli): Servis yoksa yüklemeyi durdururuz. Sistem %100 güvende kalır ama ClamAV'daki bir kesinti tüm yükleme özelliğini felç eder. (Kavun projesinde fail-fast strajesini tercih ettim)
- Fail-Safe (Erişilebilirlik Öncelikli): Servis yoksa taramayı atla ve dosyayı kabul et. Kritik olmayan sistemlerde tercih edilebilir, ancak büyük bir risk barındırır.

2. RAM Yönetimi: file.getBytes()yerine file.getInputStream() kullanın. ClamAV istemcisi veriyi chunklara bölerek işlediğinden, dosya ne kadar büyük olursa olsun bellek kullanımı sabit kalır.
3. İmzaları Güncel Tutulması: Docker üzerinde freshclam bu imzaları otomatik günceller. Ancak container her yeniden başladığında GB'larca veriyi tekrar indirmesini istemezsiniz. Docker'da imza dizinini volume olarak bağlayın. Veritabanı kalıcı hale gelir; container yeniden başlasa bile sistem güncel imzalarla anında ayağa kalkar.
4. Timeout ve Senkronizasyon İki ayarın uyumlu olması şart: ClamAV istemcisinde büyük dosyaları kaldıracak kadar bir socket timeout belirleyin (30–60 saniye). Spring tarafında ise max-file-size değeri ClamAV'ın limitinin altında kalmamalı, aksi halde dosya tarayıcıya ulaşmadan Spring tarafından reddedilir.
Kavun'da Genel Güvenlik Mimarisi
ClamAV, Kavun'un güvenlik katmanlarından sadece biri. Projenin geri kalanında:
- Spring Security ile JWT tabanlı kimlik doğrulama
- Brute-force koruması: başarısız giriş denemelerini izleme
- CORS yapılandırması: kaynak bazlı erişim kontrolü
- OWASP dependency check: bağımlılıklardaki güvenlik açıklarını tarama
- Hibernate Envers: tüm CRUD operasyonlarının detaylı audit kaydı
ClamAV bu zincirin "input güvenliği" halkasını tamamlıyor.
Dosya yükleme özelliği olan her uygulamada virüs taraması bir lüks değil, zorunluluktur. ClamAV bunu açık kaynak, ücretsiz ve production-grade bir şekilde sağlıyor.

Entegrasyon şaşırtıcı derecede basit: Docker'da bir container, Spring Boot'ta bir servis, controller'da iki satır kontrol. Karşılığında kullanıcıların yüklediği her dosya sisteme girmeden önce taranmış oluyor.