[P1 PWA] Service worker cache-firsts every same-origin GET, including API and private responses #137

Open
opened 2026-07-28 23:05:53 +00:00 by lost-rob0t · 0 comments
lost-rob0t commented 2026-07-28 23:05:53 +00:00 (Migrated from github.com)

Finding

public/sw.js sends every non-navigation same-origin GET request through cacheFirstAsset():

if (event.request.method !== "GET") return;
const url = new URL(event.request.url);
if (url.origin !== self.location.origin) return;

event.respondWith(
  event.request.mode === "navigate"
    ? networkFirstNavigation(event.request)
    : cacheFirstAsset(event.request)
);

cacheFirstAsset() then stores any successful response without checking route, content type, authorization, or cache policy:

const cached = await cacheStorage.match(request);
if (cached) return cached;
const response = await fetchRequest(request);
if (response.ok) await cache.put(request, response.clone());

This is safe only for immutable application assets. In a deployment where StarIntel APIs, model endpoints, document exports, capability discovery, auth endpoints, or other data routes share the Quasar origin, the service worker can:

  • persist private/authenticated responses in Cache Storage;
  • replay stale API data indefinitely;
  • return a prior session’s cached response after account changes;
  • bypass server revocation or updated authorization results;
  • cache large dynamic responses and grow storage without bounds.

Browser HTTP cache directives do not automatically make arbitrary Cache Storage writes safe.

Required fix

Use an allowlist for immutable build assets and make all data/auth routes network-only.

function isStaticAsset(request, url) {
  if (request.destination === "script") return /\/assets\/.*\.[a-f0-9]+\.js$/.test(url.pathname);
  if (request.destination === "style") return /\/assets\/.*\.[a-f0-9]+\.css$/.test(url.pathname);
  if (["image", "font"].includes(request.destination)) return url.pathname.startsWith("/assets/");
  return ["/manifest.webmanifest", "/favicon.svg"].includes(url.pathname);
}

function isDataOrAuthRoute(url) {
  return /^\/(?:api|v\d+|auth|oauth|documents|datasets|graphs|actors|mcp)(?:\/|$)/.test(url.pathname);
}

self.addEventListener("fetch", (event) => {
  const request = event.request;
  if (request.method !== "GET") return;

  const url = new URL(request.url);
  if (url.origin !== self.location.origin) return;

  if (request.mode === "navigate") {
    event.respondWith(networkFirstNavigation(request));
    return;
  }

  if (isDataOrAuthRoute(url) || !isStaticAsset(request, url)) return;
  event.respondWith(cacheFirstImmutableAsset(request));
});

Before storing, require a cacheable response:

function mayStore(response) {
  if (!response.ok || response.type === "opaque") return false;
  const control = response.headers.get("cache-control") || "";
  if (/no-store|private/i.test(control)) return false;
  if (response.headers.has("set-cookie")) return false;
  return true;
}

Prefer a generated precache manifest containing exact hashed build artifacts. Versioned cache cleanup should delete only Quasar-owned cache names, not every other cache under the origin.

Acceptance criteria

  • API, auth, document, graph, actor, provider, and capability responses are never written to Cache Storage.
  • Only exact immutable application assets are cache-first.
  • Cache-Control: no-store/private and credential-bearing responses are never stored.
  • Cache growth is bounded and version cleanup affects only Quasar caches.
  • Tests prove an authenticated same-origin API response is not cached or replayed.
  • Offline navigation still falls back to the app shell without caching arbitrary route responses as assets.
## Finding `public/sw.js` sends every non-navigation same-origin GET request through `cacheFirstAsset()`: ```js if (event.request.method !== "GET") return; const url = new URL(event.request.url); if (url.origin !== self.location.origin) return; event.respondWith( event.request.mode === "navigate" ? networkFirstNavigation(event.request) : cacheFirstAsset(event.request) ); ``` `cacheFirstAsset()` then stores any successful response without checking route, content type, authorization, or cache policy: ```js const cached = await cacheStorage.match(request); if (cached) return cached; const response = await fetchRequest(request); if (response.ok) await cache.put(request, response.clone()); ``` This is safe only for immutable application assets. In a deployment where StarIntel APIs, model endpoints, document exports, capability discovery, auth endpoints, or other data routes share the Quasar origin, the service worker can: - persist private/authenticated responses in Cache Storage; - replay stale API data indefinitely; - return a prior session’s cached response after account changes; - bypass server revocation or updated authorization results; - cache large dynamic responses and grow storage without bounds. Browser HTTP cache directives do not automatically make arbitrary Cache Storage writes safe. ## Required fix Use an allowlist for immutable build assets and make all data/auth routes network-only. ```js function isStaticAsset(request, url) { if (request.destination === "script") return /\/assets\/.*\.[a-f0-9]+\.js$/.test(url.pathname); if (request.destination === "style") return /\/assets\/.*\.[a-f0-9]+\.css$/.test(url.pathname); if (["image", "font"].includes(request.destination)) return url.pathname.startsWith("/assets/"); return ["/manifest.webmanifest", "/favicon.svg"].includes(url.pathname); } function isDataOrAuthRoute(url) { return /^\/(?:api|v\d+|auth|oauth|documents|datasets|graphs|actors|mcp)(?:\/|$)/.test(url.pathname); } self.addEventListener("fetch", (event) => { const request = event.request; if (request.method !== "GET") return; const url = new URL(request.url); if (url.origin !== self.location.origin) return; if (request.mode === "navigate") { event.respondWith(networkFirstNavigation(request)); return; } if (isDataOrAuthRoute(url) || !isStaticAsset(request, url)) return; event.respondWith(cacheFirstImmutableAsset(request)); }); ``` Before storing, require a cacheable response: ```js function mayStore(response) { if (!response.ok || response.type === "opaque") return false; const control = response.headers.get("cache-control") || ""; if (/no-store|private/i.test(control)) return false; if (response.headers.has("set-cookie")) return false; return true; } ``` Prefer a generated precache manifest containing exact hashed build artifacts. Versioned cache cleanup should delete only Quasar-owned cache names, not every other cache under the origin. ## Acceptance criteria - API, auth, document, graph, actor, provider, and capability responses are never written to Cache Storage. - Only exact immutable application assets are cache-first. - `Cache-Control: no-store/private` and credential-bearing responses are never stored. - Cache growth is bounded and version cleanup affects only Quasar caches. - Tests prove an authenticated same-origin API response is not cached or replayed. - Offline navigation still falls back to the app shell without caching arbitrary route responses as assets.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nsaspy/quasar-ui#137
No description provided.