← All security check guides

Is a site's storage bucket listing its files?

Most sites keep their images, downloads and uploads in object storage: an S3 bucket, a Cloud Storage bucket, an Azure container. Those files are meant to be readable, and that is not the problem. The problem is a second permission that is easy to grant by accident and hard to notice afterwards. With listing enabled, the bucket answers a plain request for its root with an index of everything inside it, and everything inside it is rarely only the images.

Why an index is not the same as a public file

A public file is found by whoever already has its address. That is what a bucket behind a website is for, and a logo nobody can reach is not much of a logo. A public index is a different thing entirely: it hands over every address at once, including the ones that were never linked from anywhere and were quietly relied on to be unfindable.

What turns up in those indexes is consistent enough to be predictable. Database exports written by a backup script that needed somewhere to put them. Invoices and statements generated per customer. User uploads, including the ones from a form that was supposed to be internal. Old versions of the site, dropped in during a migration and never removed. All of it sits in the same bucket as the images, because that was the bucket that already existed.

The exposure is also silent and durable. Nothing on the website changes, no error appears anywhere, and the storage provider records the request as an ordinary read. A bucket index is one of the first things automated scanning looks for, precisely because it costs one request and frequently returns a list worth having.

Only the storage the site itself points at

This is the most boundary-sensitive check in the product, and the constraint on it is deliberate rather than cautious. Bucket names are guessable, and a scanner that tried permutations of a company name would find other people's storage, would ask questions about resources nobody granted permission over, and would produce findings the customer cannot act on. None of that is done.

The only buckets asked about are the ones the site links to. Their addresses are read out of the page and the scripts the deep scan has already downloaded, which is the same content every visitor receives. A bucket that a verified domain publishes URLs for is part of that site's own delivery path, and that is the closest thing to ownership that can be established from outside.

Because the request goes to a storage provider's hostname rather than to the site, it sits behind the infrastructure permission rather than the website one. Reading a bucket address out of a page needs nothing more than a visitor's access; asking the provider what that bucket contains is a question about infrastructure, and it is gated on the attestation that covers infrastructure.

The request itself is one GET for the index, sent through a client that is structurally incapable of any other method. No object is downloaded, nothing is written, and no policy is modified. What survives into the report is the bucket name, the fact that listing is enabled, a count, and at most one example file name so the result can be confirmed in a browser. The index itself is counted and discarded.

How it looks in the report

The finding names the bucket, states that the index is open, and gives the number of objects it returned. The bucket name is shown deliberately: storage behind a site is frequently a supplier's rather than the owner's, and the name is what tells them whose console the fix belongs in.

It sits at high. Unlike most configuration findings this one may already have leaked something, because the index has been readable for as long as it has been enabled and there is no way to tell from outside who read it.

A 200 response is not the finding on its own. A bucket serving a static site answers its root with an index.html, and a refusal frequently arrives as a 200 carrying an error document. What is required is the provider's own listing document with at least one entry in it, so a bucket that refuses, and an empty one, both read as clean.

Example

The S3 bucket "media-assets" lists its contents to anyone

This bucket is linked from the site, and asking it for its root returns an index of what is inside. Every file name is readable, including the ones that were never linked. Turn the listing permission off, then review what the index showed.

FAIL

GET https://media-assets.s3.amazonaws.com/, HTTP 200, a S3 listing document naming 412 objects, one example key: "logo.png" (the rest were counted, not recorded)

Indexed vs closed

Indexed

A bucket created for the site's images, opened up until the images finally loaded, and never narrowed afterwards. Its root returns an XML document listing every object with its size and timestamp. Among the four hundred images are a database export from a migration, a folder of customer PDFs, and a zip of the previous version of the site.

Closed

The bucket grants read access to objects and nothing else. A request for a known file succeeds, and a request for the root is refused. The site is unaffected, because a page loads files by their address and has never needed to list them.

The two permissions are separate at every provider, which is what makes the fix small: closing the index does not take the files offline.

How to close it

  • On S3, turn Block Public Access on at the bucket and grant s3:GetObject to the public without s3:ListBucket. Those are two different actions in a bucket policy, and granting the second is what produces the index. An ACL granting READ to AllUsers on the bucket, as opposed to on its objects, does the same thing and is the older way it happens.
  • On Google Cloud Storage, check which role allUsers holds. Storage Object Viewer allows reading a file; the legacy Storage Bucket Reader role also allows listing. Remove the second and keep the first.
  • On Azure, set the container's public access level to Blob rather than Container. Blob allows a file to be read by its address; Container adds the listing, and the two are one dropdown apart.
  • Then review what the index showed, and treat it as a leak rather than as a misconfiguration. Anything in there that should not have been public has been readable for as long as the bucket has, so move it, and rotate anything it contained that can be rotated.
  • Separate the buckets while the subject is open. A bucket holding only what the website serves can be public with no consequence at all, and backups, exports and uploads belong somewhere the public has no access to by default.
  • Put a CDN in front of the public one. Serving files through a CDN rather than from the bucket's own hostname means the storage does not have to be publicly reachable at all, which removes the setting from the equation and is faster besides.
  • Re-run the audit and confirm the index is closed. Permissions at every provider come from several layers at once, and a policy that was tightened while an old ACL still grants the same thing looks fixed from the console.

Run a free scan

The free passive scan grades headers, TLS and known CVEs on any site in about thirty seconds, with no signup. Verifying that a domain is owned, and granting the infrastructure permission, adds the bucket check, which asks only the storage the site itself links to and reports the count rather than the index.

Check my website

This one needs an active scan

A passive scan reads what a site shows every visitor, and this finding is not in that layer. It takes an active audit, run by the owner on a domain they have verified, to look where it hides.

How the deep audit works →

FAQ

The files in the bucket are meant to be public. Is the listing still a problem?
Yes, because the index covers everything in the bucket rather than the files that were meant to be public. The images stay readable either way; what changes is whether the backup that was dropped in beside them can be found without knowing its name.
Does the scan look for buckets by guessing names?
No. Only buckets the site itself links to are asked about, read from the page and the scripts the site already serves to every visitor. Guessing at names would mean probing storage nobody granted permission over.
Are any files downloaded to confirm the finding?
No. One request is made for the index, the entries are counted, and one file name is kept so the result can be checked in a browser. No object is fetched, and nothing is written or changed.
Why does this need the infrastructure permission rather than the website one?
Because the request goes to the storage provider rather than to the site. Reading a bucket address out of a public page needs no special access, but asking what that bucket contains is a question about infrastructure, and it is gated on the permission that covers infrastructure.

Related checks