WSTG - Latest

Cloud Storage

ID
WSTG-CONF-11

Summary

Cloud storage services (AWS S3, Azure Blob Storage, Google Cloud Storage, and equivalents) allow web applications to store and access objects without running their own file servers. Improper access control configuration, however, may lead to the exposure of sensitive information, data tampering, or unauthorized access. By default, storage on all three major providers is private and accessible only to explicitly granted identities, but a bucket/container, or individual objects within it, can be made public - either deliberately (and too broadly) or by mistake - allowing an unauthorized user to read, upload, or modify data.

Beyond bucket-level public access, this class of issue also covers: ACL/IAM policy misconfigurations that grant broader-than-intended access to specific principals; weaknesses in signed/presigned URLs used to grant time-limited access without full credentials; and object versioning/lifecycle configuration that can leave older or supposedly-deleted sensitive data recoverable, or move it into locations with weaker access controls.

Test Objectives

  • Assess that the access control configuration for the storage services (bucket/container-level public access, and object/principal-level ACLs or IAM policies) is properly in place.
  • Assess whether signed/presigned URLs are scoped, time-limited, and handled appropriately.
  • Assess whether object versioning and lifecycle configuration could expose old or supposedly-removed sensitive data.

How to Test

First, identify the bucket/container URL, typically found via references in HTTP responses (<img>/<script> tags, JS bundles, API responses) for black-box testing, or via provider console/source code/documentation for gray-box testing. Then run the same three probes against each provider: list (does the bucket/container expose its contents without authentication?), get (can a specific/guessed object be read?), and write (can an arbitrary object be uploaded or overwritten?). Test list and get even when the bucket name isn’t already known; guessable and common names are covered under Cloud Object Storage Artifacts rather than duplicated here.

The tables below show both black-box probes (curl, no authentication assumed) and gray-box probes (provider command-line tools where credentials are available). Use curl for unauthenticated access testing; use provider CLIs where credentials are in scope to confirm configured permissions directly.

Amazon S3

URL formats: virtual-hosted style (https://<bucket>.s3.<region>.amazonaws.com/<key>) or path-style (https://s3.<region>.amazonaws.com/<bucket>/<key>); the legacy global endpoint (no region) also works for some regions.

Probe curl AWS CLI
List curl "https://<bucket>.s3.amazonaws.com/" aws s3 ls s3://<bucket>
Get curl "https://<bucket>.s3.amazonaws.com/<key>" aws s3 cp s3://<bucket>/<key> -
Write curl -X PUT -d 'test' "https://<bucket>.s3.amazonaws.com/test.txt" aws s3 cp test.txt s3://<bucket>/test.txt

A successful unauthenticated list returns an XML <ListBucketResult> body; a failed write returns AccessDenied in the response body/CLI error.

Azure Blob Storage

URL format: https://<account-name>.blob.core.windows.net/<container-name>/<blob-name>.

Probe curl Azure CLI (gray-box)
List curl "https://<account>.blob.core.windows.net/<container>?restype=container&comp=list" az storage blob list --container-name <container>
Get curl "https://<account>.blob.core.windows.net/<container>/<blob>" az storage blob download --container-name <container> --name <blob>
Write curl -X PUT -d 'test' -H "x-ms-blob-type: BlockBlob" -H "x-ms-version: <current>" "https://<account>.blob.core.windows.net/<container>/test.txt" az storage blob upload --container-name <container> --name test.txt

A 200 with an XML <EnumerationResults> body on the list probe means the container’s public access level is Container (public read + list), not Private or Blob (read of known names only). az storage container show-permission confirms the configured level directly where credentials are available.

Google Cloud Storage (GCS)

URL format (public XML-style endpoint): https://storage.googleapis.com/<bucket-name>/<object-name>.

Probe curl gsutil (gray-box)
List curl "https://storage.googleapis.com/storage/v1/b/<bucket>/o" gsutil ls gs://<bucket>
Get curl "https://storage.googleapis.com/<bucket>/<object>" gsutil cat gs://<bucket>/<object>
Write curl -X POST --data-binary @test.txt "https://storage.googleapis.com/upload/storage/v1/b/<bucket>/o?uploadType=media&name=test.txt" gsutil cp test.txt gs://<bucket>/test.txt

A 200 with a JSON object list on the list probe means public listing is enabled, commonly via an allUsers/allAuthenticatedUsers IAM binding or legacy ACL rather than GCS’s default. gsutil iam get gs://<bucket> confirms the binding directly where credentials are available.

On Windows, replace single quotes (') with double quotes (") in the curl commands above.

ACL and IAM Policy Misconfigurations

Beyond a simple public/private toggle, all three providers support fine-grained access grants that are easy to over-scope:

  • AWS S3: review bucket policies and ACLs for grants to AllUsers/AuthenticatedUsers (the S3 equivalent of “public”), and for bucket policies that grant broad s3:* or s3:GetObject/s3:PutObject to a wildcard principal ("Principal": "*") or to another AWS account that shouldn’t need access. Also confirm S3 Block Public Access is enabled at the account/bucket level unless explicitly required otherwise - it acts as a safety net even if an individual policy is misconfigured. aws s3api get-bucket-acl and aws s3api get-bucket-policy show the effective configuration in gray-box testing.
  • Azure Blob Storage: review the container’s public access level (Private/Blob/Container) as above, and separately review any Shared Access Signature (SAS) policies and RBAC role assignments (Storage Blob Data Reader/Contributor) scoped to the storage account for grants wider than needed.
  • GCS: review IAM bindings (gsutil iam get gs://<bucket>) and any legacy per-object/per-bucket ACLs for allUsers (public) or allAuthenticatedUsers (any Google account, not just this application’s users) bindings, since the latter is often mistaken for “still private” when it is not.

In all three cases, distinguish objects that are public by design (e.g. static site assets, public downloads) from configuration, backup, or user-data objects that ended up public unintentionally - the intent behind the object, not just its reachability, determines whether a finding is real.

Signed/Presigned URL Weaknesses

Signed URLs (S3 presigned URLs, Azure SAS tokens, GCS signed URLs) let an application grant time-limited access to a private object without sharing full credentials. Review:

  • Expiry: confirm the expiry set on the signed URL (X-Amz-Expires for S3, se= for Azure SAS, Expires/X-Goog-Expires for GCS) is as short as the use case allows - a signed URL with a multi-day or multi-year expiry is effectively a permanent credential if it leaks (e.g. via browser history, referrer headers, proxy/access logs, or being shared/forwarded).
  • Scope: confirm the signature is scoped to only the specific object/action needed (e.g. GCS/S3 signatures can be scoped to a single object and a single HTTP method; Azure SAS tokens can be scoped to a whole account if generated as an account-level SAS rather than a service/container/blob-level SAS - check which level was used).
  • Leakage and reuse: check whether signed URLs are logged in plaintext by intermediate proxies, CDNs, or application logs, and whether the same signed URL is reused across multiple users/sessions rather than generated per-request - a leaked URL is valid for anyone until it expires or is revoked, and most schemes have no way to revoke a single signed URL early short of rotating the underlying key/credential.
  • Signature validation: as a basic tampering check, try modifying the object path or query parameters of a valid signed URL (e.g. pointing it at a different object while keeping the same signature) and confirm the request is rejected rather than served.

Versioning and Lifecycle Issues

Object versioning (keeping prior versions of an object after it is overwritten or deleted) and lifecycle policies (rules that transition or expire objects over time) are commonly enabled for durability/compliance reasons, but can undermine an expectation that “deleted” data is actually gone:

  • If versioning is enabled, confirm that a “deleted” object (e.g. one containing data that should be erased for compliance, or an old credential file that was rotated) is not still retrievable via a previous version ID. For S3: aws s3api list-object-versions --bucket <bucket>; for GCS: gsutil ls -a gs://<bucket>/<object>; for Azure: blob snapshots/versions via az storage blob list --include v.
  • Confirm access-control settings apply consistently across versions - a bucket/container access-control fix applied after an incident may only affect the current version, leaving older versions of the same object reachable if versioned access is handled separately by the provider.
  • Review lifecycle rules that transition objects to a different storage class/tier or a different bucket/account (e.g. archival tiers, cross-region replication) - confirm the destination has equivalent access controls, since a lifecycle transition can silently move sensitive data to a location that wasn’t in scope for the original access-control review.
  • Where an application relies on “delete” to remove sensitive data (e.g. for a user data-deletion request), confirm this results in actual removal (including all versions and any replicated/lifecycle-transitioned copies) rather than only removing the current version’s accessibility.

Tools

References