How to Scan Salesforce Attachments for Malware
If your Salesforce org accepts file uploads from users, communities, or integrations, those files aren't scanned for malware. Salesforce stores and serves them as-is — a malicious file uploaded to a Case, Account, or Experience Cloud site will sit in your org until someone downloads it.
AttachmentScanner adds malware scanning with a single API call. Files are checked against multiple antivirus engines and you get back a clear result: clean, malicious, or suspicious.
This guide covers scanning Salesforce files using Apex callouts. For the full picture on scanning strategies, see the complete guide to scanning user uploads.
Setup
Sign up for an account to get your API token and scanner URL.
Named Credential
Create a Named Credential in Setup → Named Credentials so your Apex code doesn't need to hardcode the API URL or token:
- Label: AttachmentScanner
- URL:
https://scans.attachmentscanner.com - Identity Type: Named Principal
- Authentication Protocol: Custom Header
- Header Name:
Authorization - Header Value:
Bearer your_token_here
Remote Site Setting
If you're not using Named Credentials, add your scanner URL as a Remote Site in Setup → Remote Site Settings.
Your First Scan
Test the connection with the EICAR test file — a standardised test file that every antivirus engine detects. Run this in the Developer Console's Execute Anonymous window:
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:AttachmentScanner/v1.0/scans');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody('{"url": "https://www.attachmentscanner.com/eicar.com"}');
HttpResponse res = new Http().send(req);
System.debug(res.getBody());
// {"status":"found","matches":["Eicar-Test-Signature"]}
That's it — scanning is working. Now let's scan actual Salesforce files.
Scanning a ContentVersion
Most files in Salesforce are stored as ContentVersion records. To scan one, send it as the request body:
public class AttachmentScanner {
public static String scan(Id contentVersionId) {
ContentVersion cv = [
SELECT VersionData, Title, FileExtension
FROM ContentVersion
WHERE Id = :contentVersionId
];
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:AttachmentScanner/v1.0/scans');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/octet-stream');
req.setHeader('X-Filename', cv.Title + '.' + cv.FileExtension);
req.setBodyAsBlob(cv.VersionData);
HttpResponse res = new Http().send(req);
Map<String, Object> result = (Map<String, Object>)
JSON.deserializeUntyped(res.getBody());
return (String) result.get('status');
}
}
Call it after a file is uploaded:
String status = AttachmentScanner.scan(contentVersionId);
if (status == 'found') {
// Delete or quarantine the file
}
Scanning on Upload with a Trigger
To scan every file automatically, use a trigger on ContentVersion. Since
Apex triggers can't make HTTP callouts synchronously, use a @future
method or Queueable:
trigger ContentVersionTrigger on ContentVersion (after insert) {
for (ContentVersion cv : Trigger.new) {
AttachmentScannerAsync.scanAsync(cv.Id);
}
}
public class AttachmentScannerAsync {
@future(callout=true)
public static void scanAsync(Id contentVersionId) {
String status = AttachmentScanner.scan(contentVersionId);
if (status == 'found') {
ContentDocument doc = [
SELECT Id FROM ContentDocument
WHERE LatestPublishedVersionId = :contentVersionId
];
delete doc;
// Optionally notify the admin
}
}
}
Scanning Legacy Attachments
If your org still uses the older Attachment object (common in orgs that predate Files), the pattern is the same:
public static String scanAttachment(Id attachmentId) {
Attachment att = [
SELECT Body, Name FROM Attachment WHERE Id = :attachmentId
];
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:AttachmentScanner/v1.0/scans');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/octet-stream');
req.setHeader('X-Filename', att.Name);
req.setBodyAsBlob(att.Body);
HttpResponse res = new Http().send(req);
Map<String, Object> result = (Map<String, Object>)
JSON.deserializeUntyped(res.getBody());
return (String) result.get('status');
}
Experience Cloud and Communities
If you're running an Experience Cloud site where external users upload files, scanning is especially important — you have less control over what's being uploaded. The same ContentVersion trigger approach works here. Files uploaded through Experience Cloud are stored as ContentVersion records, so the trigger catches them automatically.
Works Everywhere
AttachmentScanner is cloud-agnostic — the same API works from Salesforce Apex, Heroku, Railway, Fly.io, Vercel, or your own infrastructure. If you're building integrations that span Salesforce and other platforms, you get a single scanning API across all of them.
Getting Started
- Sign up and grab your API token
- Create a Named Credential in your Salesforce org
- Test with EICAR in the Developer Console
- Add a ContentVersion trigger to scan uploads automatically
If you need help with your integration, get in touch — we're always happy to help.
AttachmentScanner Team
Other Articles
Scan File Uploads on Vercel
AttachmentScanner Team
Scan File Uploads on Fly.io
AttachmentScanner Team
Scan File Uploads on Railway
AttachmentScanner Team