PassionWavesMedia
← All articles
AWS · 5 min read · 10 Aug 2026

Scanning S3 Uploads for Malware with Amazon GuardDuty

How we replaced a self-managed antivirus stack with GuardDuty's built-in Malware Protection for S3, and the upload-to-quarantine pipeline it now runs on.

A padlock resting on a keyboard Photo by FlyD on Unsplash

Any bucket that accepts uploads from outside your control, customer files, partner integrations, even internal tools, is a door into your environment. An infected file that lands in S3 doesn't stay contained: it flows into whatever processes that bucket next, your Lambda functions, your EC2 instances, your downstream pipelines. Scanning uploads before anything else touches them is one of the higher-leverage security controls you can put in place, and it's simpler to do today than it used to be.

What we used to do

A few years ago, GuardDuty didn't have this built in, so we evaluated the third-party options on the AWS Marketplace: ClamAV-based Lambda tools along the lines of the open-source bucket-antivirus-function project, deployed as CloudFormation StackSets across every account in the organization. It worked, but it was a lot to own: a stack to deploy and keep updated in every account, resource and tag sprawl, and a growing list of things that could quietly drift out of date. We wanted something simpler.

Why we moved to GuardDuty

When GuardDuty added Malware Protection for S3, we tore out the self-managed stack and moved onto it. The appeal wasn't just fewer resources to babysit, it's an AWS-native service you turn on rather than deploy and patch. A few things worth knowing before you plan around it:

  • You can designate a delegated GuardDuty administrator account for your whole AWS Organization, done from the Organizations management account. We use our security account for this.
  • From that delegated administrator account, GuardDuty can see and enable Malware Protection across every S3 bucket in the organization, not just the account it lives in.
  • It's a paid feature priced on scan volume, not free. Check current GuardDuty pricing against your upload volume before committing, and build a small proof of concept before you plan on running it in production.

How the pipeline actually works

Upload
   v
Staging/payload S3 bucket
   v
GuardDuty scans the new object
   v
Object gets tagged: GuardDutyMalwareScanStatus
   |  scan result also published to EventBridge
   v
EventBridge rule matches the scan result event
   v
Lambda reads the result and copies the object
   |-----> NO_THREATS_FOUND -> clean destination bucket
   |-----> THREATS_FOUND    -> quarantine bucket (alerts on new object)
  • Upload lands in a staging bucket first, not directly in whatever bucket your application actually reads from.
  • GuardDuty scans the new object automatically once Malware Protection is enabled on that bucket. When it finishes, it tags the object with a GuardDutyMalwareScanStatus key, the value is NO_THREATS_FOUND if it's clean, THREATS_FOUND if it isn't (GuardDuty also has UNSUPPORTED, ACCESS_DENIED, and FAILED for objects it couldn't fully scan, worth handling explicitly rather than letting them silently fall through).
  • The same scan result is published as an EventBridge event (source: aws.guardduty, detail-type: GuardDuty Malware Protection Object Scan Result), which is what you actually build automation on top of, rather than polling for tags.
  • A Lambda function, triggered by an EventBridge rule on that event, reads the scan result and copies the object accordingly: clean payloads go to the destination bucket your downstream systems actually read from, flagged ones go to a separate quarantine bucket.
  • We set an alert on the quarantine bucket for any new object landing there, that's the signal that something needs a human to look at it: how the file got in, whether it's part of a pattern, and how to safely delete it once you're done investigating.

The actual takeaway

The part of this that matters most isn't the scanning itself, GuardDuty handles that. It's the staging-bucket pattern: nothing your application actually depends on ever reads directly from the bucket an outsider can write to. Everything sits in a holding area until it's been scanned and classified, and only the clean copy ever reaches the bucket your real systems touch. That separation is what actually contains the blast radius, the scan result is just the signal that decides which side of the fence a file ends up on.