Header Image How to Scan Salesforce Attachments for Malware

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

  1. Sign up and grab your API token
  2. Create a Named Credential in your Salesforce org
  3. Test with EICAR in the Developer Console
  4. Add a ContentVersion trigger to scan uploads automatically

If you need help with your integration, get in touch — we're always happy to help.

2026-04-25
Profile Image: AttachmentScanner Team AttachmentScanner Team

Other Articles