How to handle encrypted and password-protected files during scan
Encryption is a well-known antivirus evasion technique. An attacker can take any malicious file, compress it into a password-protected ZIP archive, and upload it to your application. Because the archive is encrypted, the antivirus scanner cannot see the contents — the file may pass as clean.
This is not a flaw in pompelmi or ClamAV. No antivirus engine can scan inside an encrypted file without the key. The question is how your application should respond.
How ClamAV behaves with encrypted files
The behaviour depends on the file type:
| File type | ClamAV behaviour | pompelmi Verdict |
|---|---|---|
| Password-protected ZIP | Detects the encryption and reports it as encrypted. Does not scan contents. | Verdict.ScanError |
| Password-protected RAR | Same as ZIP — detects encryption, cannot scan contents. | Verdict.ScanError |
| Password-protected 7-Zip (.7z) | Detects encryption, reports it, cannot scan contents. | Verdict.ScanError |
| Encrypted Office doc (password set via Office) | Office password protection uses OOXML encryption. ClamAV cannot read the contents — may return Clean (false negative). | Verdict.Clean (unreliable) |
| PDF with encryption (user password) | ClamAV reads the PDF structure but cannot access encrypted content streams. | Verdict.Clean (unreliable) |
Verdict.Clean — not because the file is safe, but because it
cannot see inside the encryption. Do not store or serve encrypted Office
files without additional controls.
Understanding the Verdict.ScanError for archives
When ClamAV encounters a password-protected archive, it exits with code 2
(scan error). pompelmi maps this to Verdict.ScanError. This is
the correct and expected behaviour — the file is not confirmed safe, so it
should not be passed through.
If you already follow the standard pompelmi handling pattern — rejecting
Verdict.ScanError — you are already protected against
password-protected archives:
const verdict = await scan(tmpPath);
if (verdict === Verdict.Malicious) {
// Confirmed threat
return res.status(400).json({ error: 'Malware detected.' });
}
if (verdict === Verdict.ScanError) {
// Includes: encrypted archives, corrupted files, scan timeouts
return res.status(422).json({ error: 'File could not be scanned. Upload rejected.' });
}
// Verdict.Clean — proceed
Detecting encrypted Office documents
Because ClamAV may return Verdict.Clean for encrypted Office
files, you need an additional check. Encrypted Office documents (created with
a password in Microsoft Office) use the OOXML encryption wrapper format. This
format stores the encrypted document inside an OLE2 compound file — meaning
the file starts with the OLE2 magic bytes D0 CF 11 E0, even
though the content is an OOXML document.
A practical heuristic: if the uploaded file has extension .xlsx,
.docx, or .pptx but its magic bytes identify it as
OLE2 (not ZIP), it is likely encrypted.
function isEncryptedOffice(filePath, originalName) {
const fd = require('fs').openSync(filePath, 'r');
const buf = Buffer.alloc(4);
require('fs').readSync(fd, buf, 0, 4, 0);
require('fs').closeSync(fd);
const isOle2 = buf[0] === 0xD0 && buf[1] === 0xCF; // OLE2 magic
const ext = originalName.split('.').pop()?.toLowerCase();
const ooxml = ['docx', 'xlsx', 'pptx', 'docm', 'xlsm'];
// OOXML extensions with OLE2 magic = almost certainly encrypted
return isOle2 && ooxml.includes(ext);
}
.doc / .xls
(legacy binary) files. If you want to accept those, check the extension list.
Generally, rejecting all OLE2 files with OOXML extensions is the safer policy.
Policy options
There is no universally correct policy. Choose based on your application's needs:
| Policy | Pros | Cons |
|---|---|---|
| Reject all encrypted files. Reject on ScanError and detect encrypted Office docs separately. |
Maximum security. No encrypted file ever enters storage. | Legitimate users with password-protected documents cannot upload them. |
| Accept encrypted files with warning. Tag them as "unverified" and restrict access until manually reviewed. |
Supports legitimate use cases (e.g. secure document sharing). | Requires a manual review workflow. Encrypted malware can enter storage temporarily. |
| Accept only specific encrypted types. Allow password-protected PDFs but reject encrypted Office docs. |
Balanced approach for specific use cases. | Requires per-format detection logic. PDFs may still carry threats. |
For most web applications the recommended policy is the first: reject all files that cannot be fully scanned.
Complete upload handler with encrypted file handling
const express = require('express');
const multer = require('multer');
const { scan, Verdict } = require('pompelmi');
const fs = require('fs');
const os = require('os');
const app = express();
const upload = multer({
dest: os.tmpdir(),
limits: { fileSize: 50 * 1024 * 1024 },
});
function readMagic(filePath) {
const fd = fs.openSync(filePath, 'r');
const buf = Buffer.alloc(4);
fs.readSync(fd, buf, 0, 4, 0);
fs.closeSync(fd);
return buf;
}
function isEncryptedOfficeDocument(filePath, originalName) {
const magic = readMagic(filePath);
const isOle2 = magic[0] === 0xD0 && magic[1] === 0xCF;
const ext = originalName.split('.').pop()?.toLowerCase();
const ooxml = new Set(['docx', 'xlsx', 'pptx', 'docm', 'xlsm', 'pptm']);
return isOle2 && ooxml.has(ext);
}
app.post('/upload', upload.single('file'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No file provided.' });
const tmpPath = req.file.path;
try {
// Pre-check: reject encrypted Office documents before scanning
if (isEncryptedOfficeDocument(tmpPath, req.file.originalname)) {
return res.status(400).json({
error: 'Encrypted or password-protected Office files cannot be accepted. ' +
'Please remove the password before uploading.',
});
}
const verdict = await scan(tmpPath);
if (verdict === Verdict.Malicious) {
return res.status(400).json({ error: 'Malware detected. Upload rejected.' });
}
if (verdict === Verdict.ScanError) {
// This catches password-protected archives (ZIP, RAR, 7z)
// as well as corrupted files and scan failures
return res.status(422).json({
error: 'File could not be fully scanned — it may be encrypted, ' +
'password-protected, or corrupted. Upload rejected.',
});
}
// Verdict.Clean — file was fully scannable and no threats found
return res.json({ status: 'ok', file: req.file.originalname });
} finally {
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
}
});
Next steps
- Worried about ZIP bombs in archive uploads? See Preventing ZIP Bomb attacks in Node.js.
- Need to scan Excel and CSV files specifically? See Scanning Excel/CSV files for malicious macros.
- Want the complete upload security checklist? Read Node.js file upload security checklist.