Serverseitiges Malware-Scanning mit ClamAV in Node.js umsetzen
In diesem DevTip zeigen wir, wie Sie serverseitiges Malware-Scanning mit ClamAV in einer Node.js-Anwendung umsetzen. Durch die Integration von ClamAV können Sie hochgeladene Dateien auf Malware prüfen, bevor Sie sie verarbeiten oder speichern, und so die Sicherheit Ihrer Webanwendung deutlich erhöhen.
Was ist ClamAV?
ClamAV ist eine Open-Source-Antiviren-Engine, die Trojaner, Viren, Malware und andere schädliche Bedrohungen erkennt. Sie wird häufig zum Scannen an Mail-Gateways eingesetzt und lässt sich für Dateiprüfungen in verschiedene Anwendungen integrieren.
Warum serverseitiges Malware-Scanning umsetzen?
Serverseitiges Malware-Scanning ist aus diesen Gründen entscheidend:
- Schutz Ihres Servers vor schädlichen Dateien
- Verhinderung der Verbreitung von Malware an andere Nutzer
- Wahrung der Integrität Ihrer Anwendung
- Einhaltung von Sicherheitsstandards und Vorschriften
Indem Sie Dateien auf dem Server scannen, fügen Sie Ihrer Webanwendung eine unverzichtbare Sicherheitsebene hinzu.
ClamAV auf Ihrem Server einrichten
Bevor Sie ClamAV in Node.js integrieren, installieren Sie es auf Ihrem Server. Verwenden Sie unter Ubuntu die folgenden Befehle:
sudo apt-get update
sudo apt-get install clamav clamav-daemon
sudo systemctl stop clamav-freshclam
sudo freshclam
sudo systemctl start clamav-freshclam
sudo systemctl start clamav-daemon
sudo systemctl enable clamav-daemon
Warten Sie, bis der erste Download der Virendatenbank abgeschlossen ist, bevor Sie den Scanner
starten. Stoppen Sie den automatischen Updater, bevor Sie freshclam manuell ausführen, damit beide nicht um dessen Sperre konkurrieren.
Die Beispiele verwenden den Socket /var/run/clamav/clamd.ctl von Ubuntu. Prüfen Sie die Einstellung LocalSocket in
/etc/clamav/clamd.conf und erteilen Sie dem Benutzer Ihrer Anwendung die Berechtigung, sich damit zu verbinden.
Konfigurieren Sie die Limits des Daemons, bevor Sie Uploads annehmen:
StreamMaxLength 26M
MaxFileSize 25M
MaxScanSize 100M
AlertExceedsMax yes
Starten Sie clamav-daemon nach jeder Konfigurationsänderung neu. AlertExceedsMax macht Verstöße gegen
unterstützte Scan-Limits als Funde vom Typ Heuristics.Limits.Exceeded sichtbar. Weisen Sie diese als nicht
eindeutig zurück; eine Datei, die nicht vollständig gescannt werden konnte, darf niemals als sauber
gemeldet werden. In der ClamAV-Dokumentation zum Scannen finden Sie
die Limits und weitere Gründe, warum ein Scan unvollständig sein kann.
ClamAV in Node.js integrieren
Diese Beispiele verwenden clamscan@2.4.0 und CommonJS-Dateien mit der Endung .cjs:
npm install clamscan@2.4.0 express@5 multer@2
Speichern Sie diesen Wrapper als ClamAVScanner.cjs. Version 2.4.0 unterstützt scanFile und scanStream,
bietet aber keine Methode scanBuffer. Dieser Wrapper sendet sowohl Dateien als auch Buffer über scanStream,
sodass der Daemon keinen Dateisystemzugriff auf das private Upload-Verzeichnis der Anwendung
benötigt:
const ClamScan = require('clamscan')
const { createReadStream } = require('node:fs')
const { stat } = require('node:fs/promises')
const { Readable } = require('node:stream')
const MAX_FILE_BYTES = 25 * 1024 * 1024
class ClamAVScanner {
constructor() {
this.clamscan = null
this.isInitialized = false
}
async initialize() {
try {
this.clamscan = await new ClamScan().init({
removeInfected: false,
quarantineInfected: false,
scanLog: null,
debugMode: false,
fileList: null,
scanRecursively: true,
clamscan: {
path: '/usr/bin/clamscan',
db: null,
scanArchives: true,
active: false,
},
preference: 'clamdscan',
clamdscan: {
socket: '/var/run/clamav/clamd.ctl',
timeout: 60000,
localFallback: false,
path: '/usr/bin/clamdscan',
configFile: null,
multiscan: true,
reloadDb: false,
},
})
this.isInitialized = true
} catch (err) {
if (err.message.includes('virus database is empty')) {
console.error('ClamAV database is not initialized. Please run freshclam')
} else if (err.code === 'ENOENT') {
console.error('ClamAV socket not found. Check if clamd is running')
} else {
console.error('ClamAV initialization failed')
}
throw err
}
}
async scanFile(filePath) {
const info = await stat(filePath)
if (!info.isFile() || info.size === 0 || info.size > MAX_FILE_BYTES) {
throw new Error('File is empty, invalid, or exceeds the scan limit')
}
return this.scanStream(createReadStream(filePath))
}
async scanBuffer(buffer) {
if (!Buffer.isBuffer(buffer) || buffer.length === 0 || buffer.length > MAX_FILE_BYTES) {
throw new Error('Buffer is empty, invalid, or exceeds the scan limit')
}
return this.scanStream(Readable.from([buffer]))
}
async scanStream(readStream) {
if (!this.isInitialized) {
readStream.destroy()
throw new Error('ClamAV scanner not initialized')
}
let timer
// clamscan 2.4 attaches input listeners only after its socket has connected.
const streamError = new Promise((_, reject) => readStream.once('error', reject))
try {
// The package's socket timeout alone does not settle every scan failure path.
const result = await Promise.race([
streamError,
this.clamscan.scanStream(readStream),
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('Scan timed out')), 60000)
}),
])
if (
!result ||
result.timeout === true ||
(result.isInfected !== true && result.isInfected !== false) ||
!Array.isArray(result.viruses)
) {
throw new Error('Scan result is inconclusive')
}
const { isInfected, viruses } = result
if (viruses.some((name) => name.startsWith('Heuristics.Limits.Exceeded'))) {
throw new Error('Scan limit exceeded; result is inconclusive')
}
return { isInfected, viruses }
} finally {
clearTimeout(timer)
readStream.destroy()
}
}
}
module.exports = ClamAVScanner
Hochgeladene Dateien mit ClamAV scannen
Integrieren Sie den Scanner mit einer sauberen Fehlerbehandlung in eine Express.js-Anwendung:
const express = require('express')
const multer = require('multer')
const { mkdir, rm } = require('node:fs/promises')
const ClamAVScanner = require('./ClamAVScanner.cjs')
const app = express()
const upload = multer({
dest: 'uploads/',
limits: {
fileSize: 25 * 1024 * 1024, // 25MB limit
},
})
const scanner = new ClamAVScanner()
let scannerInitialized = false
function diagnosticCode(error) {
// Never log arbitrary messages, paths, or third-party error payloads.
return ['ENOENT', 'EACCES', 'EEXIST', 'ECONNREFUSED', 'ETIMEDOUT'].includes(error?.code)
? error.code
: 'SCAN_FAILED'
}
// Initialize the ClamAV scanner with retry logic
const initializeScanner = async (retries = 3, delay = 5000) => {
for (let i = 0; i < retries; i++) {
try {
await scanner.initialize()
scannerInitialized = true
console.log('ClamAV scanner initialized successfully')
return
} catch (err) {
console.error(`Failed to initialize ClamAV (attempt ${i + 1}/${retries}):`, {
code: diagnosticCode(err),
})
if (i < retries - 1) await new Promise((resolve) => setTimeout(resolve, delay))
}
}
process.exit(1)
}
app.post(
'/upload',
(req, res, next) => {
if (!scannerInitialized) {
return res.status(503).json({
error: 'Scanner not initialized',
message: 'The virus scanner is not ready. Please try again later.',
})
}
next()
},
upload.single('file'),
async (req, res, next) => {
if (!req.file) {
return res.status(400).json({
error: 'No file uploaded',
message: 'Please provide a file to scan.',
})
}
try {
const scanResult = await scanner.scanFile(req.file.path)
if (scanResult.isInfected) {
return res.status(403).json({
error: 'Malware detected',
message: 'The uploaded file contains malware.',
})
}
// A completed scan found no known malware; this does not prove the file is safe.
res.status(200).json({
message: 'Scan completed; no known malware detected',
file: {
name: req.file.originalname,
size: req.file.size,
},
})
} catch (error) {
console.error('File scan could not be completed', { code: diagnosticCode(error) })
res.status(503).json({
error: 'Scan failed',
message: 'An error occurred while scanning the file.',
})
} finally {
// This endpoint only scans; it does not retain uploads after any outcome.
await rm(req.file.path, { force: true }).catch((error) => {
console.error('Temporary file cleanup failed', { code: diagnosticCode(error) })
})
}
},
)
app.use((error, req, res, next) => {
if (res.headersSent) return next(error)
const tooLarge = error instanceof multer.MulterError && error.code === 'LIMIT_FILE_SIZE'
res.status(tooLarge ? 413 : 400).json({ error: 'Upload could not be processed' })
})
async function main() {
await mkdir('uploads', { recursive: true, mode: 0o700 })
await initializeScanner()
app.listen(3000, () => console.log('Server running on port 3000'))
}
main().catch((error) => {
console.error('Server startup failed', { code: diagnosticCode(error) })
process.exitCode = 1
})
Bewährte Verfahren und Performance-Tipps
-
ClamAV regelmäßig aktualisieren: Lassen Sie den Dienst
clamav-freshclamlaufen und überwachen Sie die Aktualität der Datenbank. Planen Sie keinen zweiten Updater ein, solange dieser Dienst aktiv ist. -
Dateigrößenlimits umsetzen: Stimmen Sie die Limits Ihrer Anwendung auf
StreamMaxLength,MaxFileSizeundMaxScanSizein Ihrer Daemon-Konfiguration ab. Sie steuern unterschiedliche Limits, unter anderem für entpackte Archivinhalte:const upload = multer({ limits: { fileSize: 25 * 1024 * 1024 }, // 25MB }) -
Stream-Scanning verwenden: Setzen Sie das Scannen bei großen Dateien auf Streams um:
const result = await scanner.scanStream(readStream)Halten Sie die Quelle privat und unveränderlich, bis der Scan abgeschlossen ist. Erzwingen Sie Upload-Limits vor dem Scannen; geben Sie vor einem eindeutigen Ergebnis keine Bytes an Konsumenten weiter.
-
Batch-Verarbeitung umsetzen: Scannen Sie Dateien mit begrenzter Nebenläufigkeit. Dieses sequenzielle Beispiel behält die Größen- und Ergebnisprüfungen des Wrappers bei:
const scanFiles = async (files) => { const results = [] for (const file of files) results.push(await scanner.scanFile(file)) return results } -
Den Daemon privat halten: Bevorzugen Sie einen lokalen Unix-Socket. Das TCP-Protokoll von Clamd hat keine integrierte Authentifizierung; machen Sie es niemals in nicht vertrauenswürdigen Netzwerken zugänglich.
-
Systemressourcen überwachen: ClamAV kann ressourcenintensiv sein. Überwachen Sie die Speichernutzung und reagieren Sie entsprechend:
const os = require('os') const freeMem = os.freemem() / (1024 * 1024) // Free memory in MB if (freeMem < 100) { console.warn('Low memory warning') }
Häufige Probleme beheben
-
Probleme mit der Socket-Verbindung:
if (error.code === 'ENOENT') { console.error('Socket not found. Check ClamAV daemon status:') console.error('sudo systemctl status clamav-daemon') } -
Probleme bei Datenbank-Updates:
if (error.message.includes('virus database is empty')) { console.error('ClamAV database is empty. Run: sudo freshclam') } -
Berechtigungsprobleme:
if (error.code === 'EACCES') { console.error('Permission denied. Check file and socket permissions') }
Bewährte Verfahren für die Fehlerbehandlung
Wenn Sie ClamAV in Ihre Anwendung integrieren, sollten Sie diese zusätzlichen Strategien zur Fehlerbehandlung berücksichtigen:
-
Fehler bei Datenbank-Updates behandeln:
if (error && error.message.includes('virus database is empty')) { console.error('ClamAV database is not initialized. Please run freshclam') } -
Fehler bei Socket-Verbindungen behandeln:
if (error && error.code === 'ENOENT') { console.error('ClamAV socket not found. Check if clamd is running') }
Mit diesen Prüfungen stellen Sie sicher, dass Ihr Scanner korrekt initialisiert wird und dass Konfigurationsprobleme umgehend behoben werden.
Fazit
Serverseitiges Malware-Scanning mit ClamAV in Node.js erhöht die Sicherheit Ihrer Webanwendungen erheblich. Wenn Sie diese bewährten Verfahren, Performance-Optimierungen und robusten Maßnahmen zur Fehlerbehandlung befolgen, können Sie ein widerstandsfähiges System für Dateiscans aufbauen, das Ihren Server und Ihre Nutzer vor potenziellen Bedrohungen schützt.
Wenn Sie eine umfassendere Lösung für die sichere Verarbeitung von Datei-Uploads suchen, sollten Sie Transloadit in Betracht ziehen. Transloadit bietet robuste Funktionen zur Dateiverarbeitung, einschließlich Virenscans, die sich problemlos in Ihre Anwendungen integrieren lassen.
