Public file storage link still works after you meant to lock it down
For customers who shared a file storage link publicly and now need to stop open access. This runbook helps you identify whether the file is still public because of the link type, bucket/container permissions, CDN caching, or a leaked direct URL, then apply the right fix and verify it.
TL;DR — If a storage link can still be opened by anyone, the most common reason is that the file itself or its bucket/container is still publicly readable, or a long-lived signed URL is still valid. Start by checking the object URL in a private/incognito window and then review your storage permissions in your provider dashboard; in many cases, changing the file to private is not enough if a CDN cache or old signed link is still serving it. Reading time: ~6 min
The scenario
It is a normal Tuesday afternoon and you have just removed a public file link from an email campaign or project page because the document should no longer be open to everyone. You test the app while logged in and it looks fine, but a colleague pastes the old link into a private browser window and the file still opens instantly. Now you are wondering whether the file is still public, whether a cached copy is being served, or whether the old signed link is still valid. You need a quick way to confirm what is actually exposed and shut it down without guessing.
Symptoms
- Opening the old link in a private/incognito browser window still downloads or previews the file.
- No login prompt appears; the file opens directly.
curlto the link returns a success status such as:
HTTP/1.1 200 OK
- Or the response shows a redirect and then success:
HTTP/1.1 302 Found
Location: https://cdn.example.com/files/report.pdf
followed by:
HTTP/2 200
- The storage object URL works even when the app page that linked to it has been removed.
- A signed URL (temporary URL with a token in the query string) still works until its expiry time.
- After changing permissions in the dashboard, the link still works for some users because a CDN (content delivery network, a cache layer in front of storage) is serving an old copy.
Likely causes
| Cause | How common | Quick check |
|---|---|---|
| The file or bucket/container is still publicly readable | Very common | In your storage provider dashboard, open the bucket/container and check Permissions/Access for "Public" or "Anonymous read" |
| You shared a signed URL that has not expired yet | Very common | Look at the link itself; if it has a token/signature and expiry query parameters, it is a signed URL |
| A CDN or proxy is caching the file after you changed permissions | Common | Run curl -I "https://your-link" and look for headers like Age:, X-Cache: HIT, or a CDN hostname in Via: |
| The app page was removed, but the direct object URL is still reachable | Common | Copy the final URL after redirects and open it directly in a private window |
| More than one copy of the file exists in different paths/buckets | Less common | Search for the filename in your provider dashboard storage browser |
Step-by-step diagnosis
-
Open the exact old link in a private/incognito window.
- If the file opens without any login, continue to step 2.
- If it fails there but works only when you are logged into your app, this is probably not a public storage issue.
-
Check the HTTP response headers.
curl -I -L "https://your-old-link.example/path/to/file.pdf"
- If you see
HTTP/1.1 200 OKorHTTP/2 200at the end of the redirect chain, the file is still being served. - If you see cache headers such as
Age: 1234,X-Cache: HIT, or a CDN hostname inVia:, jump to Fixes → CDN or proxy cache is still serving the file. - If the final URL is a storage URL or direct object path, continue to step 3.
-
Look at the URL itself for a tokenized or signed link.
- Common signs are long query strings such as
?token=...,?signature=...,?X-Amz-Expires=...,?Expires=..., or similar. - If the link clearly contains a signature/token and expiry fields, jump to Fixes → A signed URL is still valid.
- If it is a plain object URL with no token, continue to step 4.
- Common signs are long query strings such as
-
Check the storage permissions in your provider dashboard.
- In your provider's dashboard, open Storage/Buckets (or Containers) → your bucket/container → Permissions/Access.
- Then open the object/file details if your provider supports per-object ACLs (access control lists, file-level permissions).
- If you see settings like Public, Anonymous read, Public bucket, or a policy allowing
GetObject/read for everyone, jump to Fixes → The file or bucket/container is publicly readable. - If the bucket is private and the object is private, continue to step 5.
-
Check whether the app page was removed but the direct file URL still exists.
- In the browser dev tools or from the
curl -I -Loutput, copy the final URL after redirects. - Open that final URL directly in a private/incognito window.
- If it still works, jump to Fixes → The direct object URL is still reachable even though the app page changed.
- In the browser dev tools or from the
-
Search for duplicate copies of the file.
- In your provider dashboard, use the storage browser search for the exact filename or object key.
- If you find the same file in multiple buckets, folders, or versions, jump to Fixes → There are duplicate copies of the file.
View as text
[Private Browser] ---- 1. open old link ----> [Old URL]
|
v
[Redirect / Final URL]
|
2. curl -I -L | 3. inspect token
v
[Storage or CDN Response]
| |
v v
[Cache headers found] [Public or signed access]Fixes
The file or bucket/container is publicly readable
Use your provider dashboard first:
- Go to Storage/Buckets (or Containers) → your bucket/container → Permissions/Access.
- Change bucket/container access from Public to Private.
- If your provider supports file-level ACLs, open the file/object and remove public read access.
- If there is a bucket policy or access policy, remove any statement that allows anonymous/public read.
A common policy pattern to remove is any rule that grants read to everyone, for example:
{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::your-bucket/*"
}
If you manage storage with a CLI, the exact command varies by provider, but the action is the same: disable public access at the bucket level and remove object ACLs that grant public read.
Verify it worked:
curl -I "https://direct-object-url.example/file.pdf"
You want a denial such as 403 Forbidden or a redirect to authentication, not 200 OK.
A signed URL is still valid
A signed URL is designed to work without login until its expiry time. If it was generated with a long expiry, anyone with that link can keep using it until it expires.
What to do:
- In your app or backend, stop generating long-lived signed URLs.
- Regenerate links with a short expiry, such as 5 to 15 minutes.
- If your provider supports revoking the underlying signing key or rotating the credential used to sign URLs, rotate it. This invalidates old links signed with the old key, but it may also affect other integrations using that key.
⚠️ Rotating a signing key or access key can break uploads, downloads, or background jobs that still use the old key. List the systems that use that key before rotating it.
If your app stores expiry in code or config, shorten it there. Example app config:
{
"signedUrlExpirySeconds": 900
}
Verify it worked:
- Test the old signed URL in a private window after key rotation or after the expiry time passes. It should fail with
403 Forbidden,401 Unauthorized, or a signature-expired message.
CDN or proxy cache is still serving the file
If you changed storage permissions but users still get the file, a CDN may be serving a cached copy.
First, confirm with headers:
curl -I "https://your-old-link.example/path/to/file.pdf"
Look for examples like:
Age: 864
X-Cache: HIT
Via: 1.1 some-cdn
Then purge the cached path in your CDN or hosting dashboard:
- Go to your provider's dashboard, then CDN/Cache → Purge/Invalidate.
- Purge the exact file path first, for example
/path/to/file.pdf. - If you cannot purge by path, purge the full URL.
- As a last resort, purge all cache for that zone/site.
If you control the origin server, add a no-store header for sensitive files so they are not cached:
location /private-files/ {
add_header Cache-Control "private, no-store" always;
}
Verify it worked:
curl -I "https://your-old-link.example/path/to/file.pdf"
The file should now return 403, 404, or an auth redirect instead of 200, and cache headers should no longer show a cached hit for that file.
The direct object URL is still reachable even though the app page changed
Removing the link from your website or app does not disable the file itself. If the object URL is public, anyone with the direct URL can still open it.
What to do:
- Find the final object URL from
curl -I -Lor your browser network tab. - Apply the storage permission fix to that object or bucket.
- If the file should only be available to logged-in users, stop linking directly to the storage URL. Instead, route downloads through your app so it checks login first and then issues a short-lived signed URL.
A safe flow looks like this:
View as text
[User Browser] ---- 1. GET /download/123 ----> [Your App] [Your App] ---- 2. check login/permission ----> [Auth Session] [Your App] ---- 3. short signed URL ----> [Storage] [User Browser] <---- 4. temporary redirect ---- [Your App]
Verify it worked:
- Open the old direct object URL in a private window. It should no longer return the file.
There are duplicate copies of the file
Sometimes the link still works because you locked down one copy, but another copy exists in a different folder, bucket, or environment.
What to do:
- In your storage dashboard, search for the filename and compare object paths.
- Check common places:
public/,uploads/,exports/,backups/, and old environment buckets likestagingorprod-old. - Remove or lock down every public copy.
⚠️ Deleting the wrong object can break a live page or remove a file your team still needs. If you are not sure, rename or move the object to a private location first instead of deleting it.
If your provider supports object versioning, review versions too; an older version may still be accessible through a separate URL pattern.
Verify it worked:
curl -I -L "https://your-old-link.example/path/to/file.pdf"
The old link and any discovered duplicate URLs should no longer return 200 OK.
Prevention
- Use short-lived signed URLs for downloads instead of permanent public object URLs.
{
"downloadLinkType": "signed",
"signedUrlExpirySeconds": 600
}
-
Block public access at the bucket/container level and only open exceptions deliberately. In your provider dashboard, set Storage/Bucket → Permissions/Access → Private and enable any available Block public access option.
-
Add a simple external check for sensitive files after permission changes. Store one known test URL and alert if it returns
200.
curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/sensitive-test-file.pdf"
Expected result after lockdown: 403 or 404.
- Set cache rules so private file paths are never cached by the CDN.
location /private-files/ {
add_header Cache-Control "private, no-store, max-age=0" always;
}
If you use a CDN dashboard, create a rule for /private-files/* with caching disabled.
- Add a release checklist item for shared files: test old links in a private/incognito window before announcing that access has been removed.
curl -I -L "https://old-shared-link.example/file.pdf"
Only mark the task complete if the final response is not 200.
- Keep separate buckets/containers for public and private files so permissions are easier to reason about. Example naming pattern:
project-public-assets
project-private-downloads
This reduces the chance of one public policy exposing everything in a mixed bucket.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI