Protecting Private Files Behind Public Websites

Protecting Private Files Behind Public Websites

How signed URLs, storage paths, and authorization checks work together.

A public website may display private invoices, course materials, user documents, or generated reports after a customer signs in. The files may live in object storage or behind a media service, but the page that links to them is not the security boundary. Every download needs a rule that connects the requester, tenant, and specific file. A private file system should also account for shared links, browser caches, and files that remain after a record is deleted.

Classify files before serving them Decide which assets are public, which are tenant-visible, and which belong to one user or business item. A marketing image can be cached openly; an invoice should require authorization. Store this classification with the file's owning record so the API can make a consistent decision. Avoid treating an unguessable filename as access control. URLs leak through browser history, logs, and forwarded messages. A file's storage path should include tenant scope for organization, but path structure alone does not prove that a caller may download it.

Authorize at the download boundary The API should authenticate the requester and check permission for the file's owning resource before returning bytes or a signed URL. A user authorized to view one tenant's invoice should not be able to alter a file ID and receive another tenant's document. Use a single media access operation or shared policy so different pages do not implement different checks. Public files can follow a simpler path, but the classification must be explicit. Test direct download requests without navigating through the page that normally exposes the link.

Use signed URLs deliberately A short-lived signed URL can let storage or a CDN deliver a private file without routing all bytes through the application. The application must still authorize the request before issuing the URL. Limit its lifetime and scope, and avoid putting sensitive metadata in the URL. Decide whether a user may share the URL during its valid period; most signed URLs act as bearer credentials until they expire. Very long lifetimes make revocation difficult. For highly sensitive files, proxying through an authenticated service may be worth the extra cost and complexity.

Handle caching and deletion Private responses should not enter a shared cache that can serve another user or tenant. Set appropriate cache headers and review CDN configuration. A file may remain in object storage after its database record is deleted; define cleanup and retention rules. Generated files and temporary exports need expiration and background deletion. Backup copies require their own retention policy. When a user's access is revoked, new signed URLs should stop immediately, though previously issued URLs may remain valid until expiry. Choose the duration with that limitation in mind.

Keep uploads safe Validate size and accepted formats before storing uploads. Avoid using client-provided filenames as trusted paths. Scan or transform risky file types where the product's threat model warrants it, and serve downloads with headers that prevent unintended script execution. Track ownership and tenant context at creation so later authorization has reliable facts. If a file is associated with an item in another part, define how deletion or transfer of that item affects the file. An orphaned private file can become both a cost and a privacy problem.

Verify the complete path Test a public asset, a valid private download, a wrong-tenant request, a revoked user's old session, an expired signed URL, and a deleted record. Include media served through alternate domains or CDNs. Monitor unusual download volume and failed access attempts without logging sensitive file contents. For SMB SaaS, a straightforward classification, server-side authorization, and short-lived delivery link can protect private files effectively when every route follows the same contract. Review file access whenever a new public route or CDN layer is added, because the delivery path can change even when the underlying storage does not.