Every request for private file data needs authorization. That can mean checking the user’s current permissions or validating a narrowly scoped, short-lived credential issued after that check. Static routes, object-storage URLs, signed URLs, CDN caches and multi-file formats like HLS video move file delivery outside your application’s request path. This post covers where those gaps open, what signed URLs and signed cookies actually guarantee, how a CDN can serve one user’s file to another, and how to choose between an authenticated proxy, signed URLs, signed cookies and segment encryption.
The HLS example: a protected playlist with public segments
Consider a customer playing a camera recording in your app. The app delivers that recording using HTTP Live Streaming (HLS). Despite the name, HLS can deliver recorded footage as well as live video. Instead of downloading one video file, the player first requests a playlist: a text file listing the URLs of short video files called segments. The segments contain the footage; the playlist tells the player where to fetch it.
For this recording, the playlist request goes through your API. The API checks the customer’s session, verifies that the recording belongs to their organization (the tenant), and confirms that their role allows them to watch it. Only then does it return the playlist.
GET /hls/recording-456/index.m3u8 -> 200 (authorized)
Here is a simplified playlist for that recording. Each #EXTINF:4.0 line gives the duration of the segment that follows: four seconds. The relative segment URLs resolve under the same /hls/recording-456/ path.
#EXTM3U
#EXTINF:4.0,
segment-001.ts
#EXTINF:4.0,
segment-002.ts
#EXT-X-ENDLIST
The player then makes a separate HTTP request for each segment. If those segments sit on a static route or in a public bucket, their requests bypass the API’s authorization checks.
GET /hls/recording-456/segment-001.ts -> 200 (no authorization check)
The playlist was protected. The recording’s video segments were not. Anyone who obtains or guesses a segment URL can skip the playlist and fetch the footage directly. CDN vendors warn about exactly this: signing only the playlist leaves the segments open to anyone who knows how they are named.
Storage, the CDN and the player can all work as configured. The bug is that the file path was never part of the authorization model.
Files are data. If the data is private, file delivery is an authorization system, not a storage feature.
Upload, return a URL, done
Most file features start with three steps:
App uploads report.pdf to storage
-> app returns https://files.example.com/report.pdf
-> browser downloads it
Unlike the HLS recording, this report is a single file: there is no playlist and no separate segment requests. For a public report, this flow is correct and it scales. Storage is reliable, the CDN is fast, and the application never sees the bytes.
The trouble starts when the file becomes private. Every request now has to answer the same questions your API already answers for rows in a database:
- Who is this user?
- Which organization are they acting in?
- Does this file belong to that organization?
- Does their role permit access?
- Was access removed since the URL was issued?
- Should this download be logged?
- Can a shared cache store this response?
- How long should access last?
An authenticated API route can answer these because the request passes through your application. Handing out a direct storage or CDN URL moves delivery outside that route. The destination must enforce the authorization decision instead.
Every way out of the request path
Files usually reach the browser by a route your API never sees. Each of these can skip your checks:
| Route | What it skips |
|---|---|
A static /uploads/... or /clips/... route | The application entirely. The web server serves whatever it can read. |
| A bucket or object-storage URL | Your user model. Storage checks the signature or the bucket policy, not the tenant. |
| A CDN in front of either | Current permissions. Without an access check at the edge, it can replay a stored response after access is gone. |
| Derived files: thumbnails, previews, transcoded renditions | The rule on the original. A protected image can get a thumbnail at a public, guessable path. |
| A background worker that generates links | The request context. There is no session, so tenant checks are easy to forget. |
A longer HLS recording can require hundreds of segment requests. The opening example uses one media playlist, which lists segments directly. An adaptive-bitrate recording adds a multivariant playlist (also called a master playlist), which lists the available quality levels. The player selects a quality level, requests its media playlist, and fetches its segments. It can request another media playlist when it changes quality. Every referenced playlist and segment needs protection. Alternate audio, subtitles, initialization segments and encryption keys need the same treatment when present.
The test is simple. Take a file URL your own product has returned and open it in a private window with no session. A session-protected URL should fail; a valid signed URL may still work because the link itself grants access. Then test a known segment or thumbnail directly, without a session or signed credential. A denied directory listing does not prove the files inside are private. For a signed URL, repeat the test after expiry and after removing access from the user for whom it was issued.
This is the same distinction as Conformance Is Not Interoperability: a configured capability is not proof of the behavior you depend on.
A signed URL is an authorization decision that outlives the request
One common fix is a storage-signed URL. The app checks access once, signs a URL with an expiry, and storage serves the file to whoever presents it. That is a real improvement, but notice what moved. The product-level check happened at signing time. Later requests validate the signature and the storage provider’s access rules, not the user’s current membership in your product. A CDN-signed URL follows the same pattern, but the CDN validates the credential before serving the file.
Three consequences follow.
There is usually no per-link revocation switch. On S3, nothing is registered with AWS when you sign a URL. It stops working when it expires, when the signing credentials expire or are revoked, or when access to the object is denied by storage policy. Removing or replacing the object can also stop the original download. Those measures can affect other links and users; they are not the same as revoking one product user’s access. Removing a user from an organization does not, by itself, invalidate links they already hold.
The expiry is part of the permission. Microsoft’s AI research team shared a training dataset with an Azure SAS token. The token exposed 38TB of private data, including two employees’ workstation backups, and it was valid until 2051. Scope matters as much as lifetime: a long-lived token covering a whole container grants much more than a short-lived token for one object.
URLs travel. A signed URL is a bearer token. It can end up in browser history, server logs, chat messages, screenshots and, depending on referrer policy and navigation, Referer headers. A study of 21 collaboration services found seven that leaked secret document links through the Referer header. A user who forwards a bearer link forwards their access with it. Redact tokens from logs and use a restrictive referrer policy on pages that expose sensitive links.
The expiry you pick is a decision about how long a revoked user may retain access. Make it on purpose: short lifetimes for footage and sensitive documents, with renewal through a fresh authorization check. Expiry generally governs new requests, not bytes already downloaded or a transfer already in progress.
A cache is a second authorization system
Return to the camera recording. You route both the playlist and every video segment through your API, which checks the session, recording ownership and role on each request. Then traffic grows and someone puts a CDN in front of that authenticated route.
Now there are two paths. A cache miss reaches your check. A cache hit is answered at the edge from whatever was stored, and your check never runs. If the response was cached under a key that ignores who asked, the next person to request that path gets the first person’s file.
This is not hypothetical. On March 30, 2026, a configuration change at Railway cached HTTP GET responses for about 0.05% of domains that had CDN disabled, for 52 minutes. Responses with explicit Cache-Control headers were handled correctly. Responses without them were cached by default. In the community thread, one team reported students seeing other people’s accounts and deleting their data.
The lesson is not about one provider. A missing header is a decision, and someone else’s default makes it for you. Two rules follow:
- Private responses that must not be cached say so explicitly:
Cache-Control: private, no-store.privatedisallows storage by a shared cache;no-storedisallows storage by any compliant cache. Configure the CDN to honor these directives rather than overriding them with a cache-everything rule. - If you cache private files at the edge, the edge checks access on every request, hit or miss. A CDN’s signed-cookie or signed-URL feature can do this when configured to require and validate the credential before serving cached content. A plain cache in front of your own auth check does not. A storage-signed URL alone does not establish that the CDN checks it on cache hits.
The origin also needs protection. A private CDN route is not enough if callers can bypass it and fetch the same object from a public bucket.
Ship Fast. Recover Faster. argues for testing the production path rather than just the application code. For private files, that path includes both a cold cache and a warm one.
Four fixes, one test: does every segment get checked?
Every approach is a trade between checking current product permissions and trusting a credential until it expires. Run each one against the same camera recording from the opening: its playlist and every video segment.
| Approach | When access is checked | How are segments protected? | Revoked user loses access | Trade-off |
|---|---|---|---|---|
| Authenticated same-origin proxy | Every request, in your app | The proxy checks access before serving each segment; every playlist and segment path must go through it | On the next request, once the permission change reaches the check | Every byte passes through your proxy infrastructure |
| Signed URLs | Product permissions at issuance; signature at each storage or CDN request | Each segment needs a valid signed URL, usually inserted by rewriting the playlists | When the token expires or an applicable storage/CDN policy blocks it | Playlist rewriting and renewal; cache fragmentation if token parameters enter the cache key |
| Signed cookies on the CDN | Signature and policy at the edge on every request; product permissions at issuance or renewal | The CDN validates the cookie for each segment; the policy must cover every authorized playlist and segment | When the cookie expires, unless the edge also checks revocation | Browser cookie restrictions, domain setup and careful path scoping |
| Encrypted segments plus a key server | When the player fetches the decryption key | Leaked encrypted segments need a key to play | Future key requests can be refused; keys already obtained still work | Key protection and rotation; encryption does not revoke downloaded keys or footage |
AWS’s own guidance is to use signed cookies when access covers many files, and it names HLS as the example. Signed URLs fit one file, like the report download above. For HLS, rewriting the playlists to insert signed URLs must cover all referenced resources, not just the first playlist; AWS describes one such implementation.
Signed URLs do not inherently destroy cache hit rates. A CDN can validate its signing parameters and exclude them from the object cache key, letting authorized users share cached bytes. If you include each user’s token in that key, you fragment the cache. Verify your provider’s behavior rather than assuming either outcome.
Signed cookies also need a browser setup that actually sends them on every playlist and segment request. Cross-site cookie restrictions and player credential settings matter. Keep the signed policy restricted to the recordings that the user is authorized to watch, not a wildcard that exposes every tenant. Cookie path scoping for HLS is part of that setup, not a replacement for the signed policy.
For most products the answer is a combination. Use a proxy when current product permissions must be checked on each request. Use short-lived CDN credentials when an explicitly accepted revocation delay buys useful delivery scale. Keep the lifetime close to the viewing session and reauthorize renewal. Segment encryption adds a different kind of protection: it protects the contents of leaked segment files, rather than denying their download. The key server must still check access, and a player that already has the key can decrypt those segments. None of these options can take back bytes a user already received.
Files are data
Storage is rarely what fails here. Buckets are durable and CDNs are fast. What breaks is carrying your product’s authorization rules across browsers, players, CDNs, storage providers and time.
Before you ship a private file feature, check these:
- Every path that serves private bytes, including segments, thumbnails and renditions, goes through an access check.
- No private file sits on a static route or in a bucket that answers without authorization, including a route that bypasses the CDN.
- Signed URLs and cookies are narrowly scoped and short-lived, and their lifetime is written down as a decision.
- You know what happens to existing links when a user is removed, and someone has tested it.
- Every private response sends
Cache-Control: private, no-store, or the edge checks access on every hit under an intentional private-content cache policy. - Workers that generate links run the same tenant check as the API.
- Downloads of sensitive files are logged with the user and tenant where available, without logging bearer tokens. Signature-only delivery needs an explicit audit design; it does not identify the person using a forwarded link.
A file URL is not just a pointer to some bytes. For private content, it carries a permission, and it can keep working after you stop thinking about it.
Sources
- Secure HLS with URL signing (CDNsun)
- 38TB of data accidentally exposed by Microsoft AI researchers (Wiz)
- Incident report: March 30, 2026, authenticated user data cached (Railway)
- Railway community thread on the caching incident
- Why a presigned URL expires early (AWS re:Post)
- How S3 presigned URLs actually expire (StackHarbor)
- On the perils of leaking referrers in online collaboration services
- Choosing signed URLs or signed cookies (Amazon CloudFront docs)
- Secure video streaming with CloudFront signed URLs (AWS blog)
- CloudFront signed URLs or signed cookies (Barking Iguana)
- CloudFront signed cookies for HLS (Advanced Web Machinery)
- HLS encryption explained (Publitio)
- Craft Bouncer: derived files and public paths