Technical guide

How to control content in a PWA: secure uploads and secure downloads

Control PWA file transfers on the server: validate uploads, serve files from a separate origin with safe headers, keep them out of caches and test logout.

  • Locate each control

    Separate server authorisation, response headers, browser storage and any native wrapper before choosing a protection policy.

  • Isolate user content

    Treat uploaded bytes as untrusted, store them outside the app, and serve them from a separate origin as attachments.

  • Test stale clients

    Exercise old service workers, account changes, logout and interrupted transfers in every installed context.

Control PWA uploads and downloads on the server, where they can be enforced. Authorise every transfer, validate uploaded bytes, keep them outside the application’s origin and serve them back from a separate origin with Content-Disposition: attachment, X-Content-Type-Options: nosniff and Cache-Control: no-store. Keep private files out of the service worker’s cache, and clear them on logout in every context where the app runs, including iOS home-screen web apps, which keep storage separate from Safari. A hidden button or a file picker filter enforces nothing. Test any native wrapper as its own boundary.

Map browser, content and wrapper responsibilities

Draw the transfer path before changing any setting: file selection, upload request, validation, storage, retrieval and local use. Name the origin and account at each step. Keep private content apart from the public assets that let the application shell work offline.

ComponentResponsibilityBoundary to test
Browser interfaceExplain file rules and show transfer stateSkipping the interface must not skip server checks
Upload serviceAuthorise the operation and validate incoming bytesUntrusted metadata, incomplete transfers, duplicate requests
Private object storageRestrict access to approved contentDirect object URLs and cross-account access
Download serviceCheck the current user’s right to the objectExpired sessions and changed ownership
Service worker and browser storageApply the caching and retention policyLogout, account switching, old cached responses
Native wrapper, if presentMediate its own file and navigation interfacesData handed to the OS or another app

Know where storage lives

Installed web apps do not all share storage with the browser.

  • iOS and iPadOS. A web app added to the Home Screen keeps its own cookies and website data, separate from Safari. A sign-out in Safari does not sign the user out of the home-screen app, and cached files in one are invisible to the other. Test logout, account switching and cache clearing in each.
  • Android. A PWA installed from Chrome runs on Chrome’s engine and shares cookies and storage with Chrome for the same origin. A sign-out in one affects the other.
  • Native wrappers. A WebView in a store app has its own storage, file chooser and download handling, which the wrapper code controls.

Otukan protects the native wrapper of a web app packaged as an Android or iOS store app, and can close native functions such as file access to the page when the bridge between web content and native code is tampered with. Web content running in a browser or a home-screen install is secured with the web controls and server checks in this guide. For product context, see progressive web app protection; navigation boundaries are covered on the PWA deep-link guide.

Put file controls where they can be enforced

Accept uploads safely

  1. Define the file types, size limits and quotas the business needs, and enforce them on the server. Record which roles may upload what.
  2. Authorise the destination before accepting content. Store each file under a server-generated identifier; never use the supplied filename as a path.
  3. Validate the extension against an allowlist and check the content with a suitable parser. OWASP warns that the Content-Type request header is set by the client and can be spoofed.
  4. Store uploads outside the web root, ideally on separate storage. Hold unreviewed files in a restricted state until the checks your threat model requires pass.
  5. Release the object for retrieval only once validation and ownership are recorded. Delete rejected and abandoned uploads on a defined schedule.

Use purpose-built fixtures for adverse testing: a disallowed extension, mismatched content type, a truncated file, an HTML or SVG file carrying script, and an attempt to write to another account’s destination.

Serve downloads from a separate origin

An uploaded HTML or SVG file served from your app’s origin runs with that origin’s cookies, storage and service worker. Serve user content from a different registrable domain reserved for it, so a malicious file cannot reach the application’s session.

Set these headers on every user-content response:

HeaderPurpose
Content-Disposition: attachment; filename*=UTF-8''…The browser saves the file instead of rendering it (RFC 6266)
X-Content-Type-Options: nosniffThe browser uses the declared type instead of guessing from the bytes
Content-Type set by the server from validationNever echo the type the uploader supplied
Cache-Control: no-storeShared and browser HTTP caches do not keep private files (RFC 9111)
Content-Security-Policy: sandboxDefence in depth if a file is rendered anyway

For each download, check the account, the object and the current access decision, on the normal route and any direct one. If you use signed or delegated URLs, keep their lifetime short and remember that signing a URL does not make it single-use or revocable.

Keep private files out of offline storage

Cache-Control: no-store governs HTTP caches, not the service worker. The Cache API does not honour HTTP caching headers: anything your code puts there stays until your code deletes it. Exclude private transfer responses from generic caching routes. Where offline documents are a requirement, design account separation, retention and logout deletion explicitly, and test that the bytes are gone, not just that the screen is clear. On logout, delete Cache API entries and IndexedDB records for the account; a Clear-Site-Data response header can help where the browser supports it.

A file the user saves to the device leaves the application’s control. Tell users when a download does that.

Verify distribution, network paths and updates

Run the same synthetic transfer suite in the browser, the home-screen install and the signed wrapper on each operating system. Record app revision, runtime version, OS, origin, policy and account role with every result.

ExerciseExpected decision
Valid authorised upload and downloadContent reaches only the intended account
Account switched during an interrupted uploadOwnership is not silently reassigned
Another account requests the object directlyThe server denies access whatever the interface shows
Uploaded HTML or SVG opened from its URLIt downloads, or renders without access to the app’s origin
Logout, then offline reopeningPrivate content follows the retention policy in each context
Old service worker against new server rulesStale clients cannot relax server rules
Redirect, retry or network interruptionDestination and authorisation stay within the design

Repeat installation, update, sign-in and the sensitive document workflow after any change to protection, caching or distribution. Run legitimate transfers alongside adverse cases so a blanket failure is not mistaken for protection.

For recovery, stop serving affected content, keep a redacted diagnostic record and fix the failing boundary. If stale assets are involved, test the service worker update and cache clean-up with real clients; deploying new JavaScript does not remove old copies at once.

  • Server validation holds against direct requests and interface bypass.
  • User content is served from its own origin with attachment, nosniff and no-store headers.
  • Browser, home-screen and wrapper results are recorded separately per OS.
  • Logout and account switching remove stored bytes in each context.
  • Recovery is verified with both old and updated clients.

Broader integration responsibilities are covered on the integration overview.

Further reading

Common questions

  • Does a file picker filter control what users can upload to a PWA?

    No. A hidden button or a file picker filter enforces nothing. Enforce file types, size limits and quotas on the server, validate the extension against an allowlist and check the content with a suitable parser, because the Content-Type request header is set by the client and can be spoofed.

  • Why serve user downloads from a separate origin?

    Because an uploaded HTML or SVG file served from your app's origin runs with that origin's cookies, storage and service worker. Serve user content from a different registrable domain, with Content-Disposition: attachment, X-Content-Type-Options: nosniff, a server-set Content-Type and Cache-Control: no-store on every response.

  • Does Cache-Control: no-store keep files out of the service worker cache?

    No. It governs HTTP caches, not the service worker. The Cache API does not honour HTTP caching headers, so anything your code puts there stays until your code deletes it. Exclude private transfer responses from generic caching routes, and delete Cache API and IndexedDB entries for the account on logout.

  • Does signing out in Safari sign the user out of an iOS home-screen web app?

    No. On iOS and iPadOS, a web app added to the Home Screen keeps its own cookies and website data, separate from Safari, and cached files in one are invisible to the other. On Android, a PWA installed from Chrome shares cookies and storage with Chrome for the same origin.

  • What part of a PWA does Otukan cover?

    The native wrapper. Otukan protects the native wrapper of a web app packaged as an Android or iOS store app, and can close native functions such as file access to the page when the bridge is tampered with. Web content in a browser or home-screen install relies on web controls and server checks.

Define your file-transfer boundary

Tell us your platforms, browser or wrapper, framework and file-transfer requirement. Do not send application binaries, credentials, confidential files or production data.

Or request a technical demo