Secure Uploads - v1.0.0
Malware Scanning
Scanners satisfy MalwareScannerInterface (scan, isAvailable, getName). The package ships three implementations; you can also build your own — see Custom scanners.
| Driver | Use case |
|---|---|
null |
Default. No-op. Dev, CI, kill-switch. |
clamav |
Local scanning via Unix socket (preferred) or binary fallback. Private data stays on your host. |
virustotal |
Cloud scanning via VirusTotal API. Hash-first to skip uploads for already-known files. |
See Scanner setup for installing and configuring each.
When the scanner runs
The scanner runs as part of SecureFileStorageService::store() (which is what HasSecureFiles::attachSecureFile() calls):
validate → scan → write to disk → create DB row
If malwareScanning.async = true, the order changes:
validate → write to quarantine → create DB row marked pending_scan
…and the security:scan-quarantine command processes pending files later.
Sync mode (default)
$stored = $post->attachSecureFile($request->file('attachment'));
The store call blocks on the scan. If the file is infected, MalwareDetected fires and the call throws — no DB row, no file on disk.
Pros: simple, immediate user feedback. Cons: every upload waits for the scan (~tens of ms for ClamAV socket, seconds for VirusTotal cold path).
Async mode
Set in config:
'malwareScanning' => ['async' => true, /* ... */],
Or per-call:
$stored = $post->attachSecureFile($request->file('attachment'), ['async' => true]);
The file lands in the quarantine path and the SecureUploadedFile row gets scan_status = pending. Run the worker:
php artisan security:scan-quarantine
Schedule it via Laravel's scheduler so it processes the queue automatically. Files that pass move to their normal storage location; failures fire MalwareDetected and stay quarantined for review.
failOnScanError
When the scanner is unavailable (ClamAV daemon down, VirusTotal API key missing, etc.):
failOnScanError = true(default) — the upload is rejected. Fail closed.failOnScanError = false— the upload proceeds, the error is logged, and the row getsscan_status = error.
Use true in production for anything sensitive. Use false only in environments where uploads must succeed even if scanning is intermittently broken (and you have downstream review).
ScanResult
Every scan call returns a ScanResult:
$result->infected; // bool
$result->signature; // ?string — the matched virus name, when known
$result->scanner; // string — getName() of the scanner that ran
$result->scannedAt; // CarbonInterface
$result->raw; // array — raw scanner output, for debugging
The MalwareDetected event payload includes the full ScanResult so listeners can audit, alert, or escalate.
Building your own scanner
See Custom scanners.