Implementing server-side malware scanning with ClamAV in Node.js

In this DevTip, we will explore how to implement server-side malware scanning using ClamAV in a Node.js application. By integrating ClamAV, you can scan uploaded files for malware before processing or storing them, significantly enhancing your web application's security.
What is ClamAV?
ClamAV is an open-source antivirus engine designed to detect trojans, viruses, malware, and other malicious threats. As of January 2025, ClamAV's latest stable version is 1.4.2, with version 1.4.x being the current LTS release. It is widely used for mail gateway scanning and can be easily integrated into various applications for file scanning.
Why implement server-side malware scanning?
Implementing server-side malware scanning is crucial for:
- Protecting your server from malicious files
- Preventing the spread of malware to other users
- Maintaining the integrity of your application
- Complying with security standards and regulations
By scanning files on the server, you add an essential layer of security to your web application.
Setting up ClamAV on your server
Before integrating ClamAV with Node.js, install it on your server. Use the following commands on Ubuntu:
sudo apt-get update
sudo apt-get install clamav clamav-daemon
sudo systemctl stop clamav-daemon
sudo freshclam
sudo systemctl start clamav-daemon
sudo systemctl enable clamav-daemon
Note: After installation, wait a few minutes for the initial virus database download to complete.
On modern Ubuntu systems, the ClamAV socket is typically located at:
/var/run/clamav/clamd.ctl
(Ubuntu 22.04+)/var/run/clamd.scan/clamd.sock
(older versions)
Verify your specific socket path with:
sudo find /var/run -name "clamd.*"
Integrating ClamAV with Node.js
To use ClamAV in your Node.js application, install the latest version of the clamscan package (currently 2.4.0):
npm install clamscan@2.4.0
Now, let’s create a robust wrapper for ClamAV scanning:
const ClamScan = require('clamscan')
class ClamAVScanner {
constructor() {
this.clamscan = null
this.isInitialized = false
}
async initialize() {
try {
this.clamscan = await new ClamScan().init({
removeInfected: true,
quarantineInfected: false,
scanLog: null,
debugMode: false,
fileList: null,
scanRecursively: true,
clamscan: {
path: '/usr/bin/clamscan',
db: null,
scanArchives: true,
maxFileSize: 26214400, // 25MB
},
preference: 'clamdscan',
clamdscan: {
socket: '/var/run/clamav/clamd.ctl',
timeout: 60000,
localFallback: true,
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 error:', err)
}
throw err
}
}
async scanFile(filePath) {
if (!this.isInitialized) {
throw new Error('ClamAV scanner not initialized')
}
try {
const { isInfected, viruses } = await this.clamscan.scanFile(filePath)
return { isInfected, viruses }
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error('File not found or ClamAV socket connection failed')
}
console.error('Error scanning file:', error)
throw error
}
}
async scanBuffer(buffer) {
if (!this.isInitialized) {
throw new Error('ClamAV scanner not initialized')
}
try {
const { isInfected, viruses } = await this.clamscan.scanBuffer(buffer)
return { isInfected, viruses }
} catch (error) {
console.error('Error scanning buffer:', error)
throw error
}
}
}
module.exports = ClamAVScanner
Scanning uploaded files with ClamAV
Integrate the scanner into an Express.js application with proper error handling:
const express = require('express')
const multer = require('multer')
const fs = require('fs')
const path = require('path')
const ClamAVScanner = require('./ClamAVScanner')
const app = express()
const upload = multer({
dest: 'uploads/',
limits: {
fileSize: 25 * 1024 * 1024, // 25MB limit
},
})
const scanner = new ClamAVScanner()
let scannerInitialized = false
// 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}):`, err)
if (i < retries - 1) await new Promise((resolve) => setTimeout(resolve, delay))
}
}
process.exit(1)
}
initializeScanner()
app.post('/upload', upload.single('file'), async (req, res) => {
if (!scannerInitialized) {
return res.status(503).json({
error: 'Scanner not initialized',
message: 'The virus scanner is not ready. Please try again later.',
})
}
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) {
// Delete the infected file
fs.unlinkSync(req.file.path)
return res.status(403).json({
error: 'Malware detected',
message: 'The uploaded file contains malware.',
viruses: scanResult.viruses,
})
}
// File is clean, proceed with your application logic
res.status(200).json({
message: 'File uploaded and scanned successfully',
file: {
name: req.file.originalname,
size: req.file.size,
mimetype: req.file.mimetype,
},
})
} catch (error) {
// Clean up the uploaded file
if (req.file && fs.existsSync(req.file.path)) {
fs.unlinkSync(req.file.path)
}
console.error('Error during file scan:', error)
res.status(500).json({
error: 'Scan failed',
message: 'An error occurred while scanning the file.',
})
}
})
app.listen(3000, () => {
console.log('Server running on port 3000')
})
Best practices and performance tips
-
Update ClamAV Regularly: Keep your virus definitions current by using
freshclam
. Add the following entry to your crontab:0 */6 * * * /usr/bin/freshclam --quiet
-
Implement File Size Limits: ClamAV defaults to scanning files up to 25MB. Adjust this limit as needed:
const upload = multer({ limits: { fileSize: 25 * 1024 * 1024 }, // 25MB })
-
Use Stream Scanning: For large files, implement scanning on streams:
const scanStream = async (readStream) => { return new Promise((resolve, reject) => { readStream .pipe(scanner.createStream()) .on('scan-complete', (result) => resolve(result)) .on('error', (error) => reject(error)) }) }
-
Implement Batch Processing: When handling multiple files, use batch scanning for efficiency:
const scanFiles = async (files) => { return await scanner.clamscan.scanFiles(files) }
-
Consider TCP Socket Scanning: For high-volume environments, using the TCP socket method can offer improved performance over file-based scanning.
-
Monitor System Resources: ClamAV can be resource-intensive. Monitor memory usage and respond accordingly:
const os = require('os') const freeMem = os.freemem() / (1024 * 1024) // Free memory in MB if (freeMem < 100) { console.warn('Low memory warning') }
Troubleshooting common issues
-
Socket Connection Issues:
if (error.code === 'ENOENT') { console.error('Socket not found. Check ClamAV daemon status:') console.error('sudo systemctl status clamav-daemon') }
-
Database Update Issues:
if (error.message.includes('virus database is empty')) { console.error('ClamAV database is empty. Run: sudo freshclam') }
-
Permission Issues:
if (error.code === 'EACCES') { console.error('Permission denied. Check file and socket permissions') }
Error handling best practices
When integrating ClamAV into your application, consider these additional error handling strategies:
- Handle database update errors:
if (error && error.message.includes('virus database is empty')) {
console.error('ClamAV database is not initialized. Please run freshclam')
}
- Handle socket connection errors:
if (error && error.code === 'ENOENT') {
console.error('ClamAV socket not found. Check if clamd is running')
}
Implementing these checks ensures that your scanner is properly initialized and that any configuration issues are promptly addressed.
Conclusion
Implementing server-side malware scanning with ClamAV in Node.js significantly enhances the security of your web applications. By following these best practices, performance optimizations, and robust error handling measures, you can build a resilient file scanning system that protects your server and users from potential threats.
If you're looking for a more comprehensive solution for handling file uploads securely, consider using Transloadit. Transloadit offers robust file processing capabilities, including virus scanning, that can be easily integrated into your applications.