Fix push SSRF, cross-user unsub, and personalization edge cases
Code review of the personalization/feeds/PWA/push work surfaced ten confirmed issues, now fixed: - Web Push delivery bypassed the SSRF guard (unguarded default client); now routes through safehttp.NewClient with a hard timeout, and the subscribe handler validates the endpoint URL. - Push unsubscribe deleted by endpoint with no owner check; added RemovePushSubscriptionForUser scoped to the signed-in user. - Byte-slice body/content truncation could split a UTF-8 rune and break the RSS content:encoded XML; added a rune-safe truncateUTF8 helper. - Digest sender could permanently starve a user who hid a high-volume source; step the watermark past a full hidden-source scan window. - Service worker cached personalized HTML navigations into a shared cache (identity leak across PWA users); navigations are now network-only, CACHE_VERSION bumped to v2 to purge stale pages. - Public /api/article leaked discarded/unclassified bodies; filter to classified, non-sentinel stories. - runLocal never started the push sender; digests now fire in -local. - Push client had no timeout, so one hung endpoint stalled all sends. - Reader migration resurrected cross-device-cleared reads; gate it behind a one-time flag so the server stays authoritative. - Bookmarks count didn't match the classified list filter.
This commit is contained in:
@@ -143,10 +143,20 @@
|
||||
serverRead[id] = 1; readSet[id] = 1; paintCard(id, true);
|
||||
});
|
||||
(data.bookmarked || []).forEach(function (id) { setBookmarkQuiet(id, true); });
|
||||
// Push up reads made on this device before the account knew them.
|
||||
ids.forEach(function (id) {
|
||||
if (readSet[id] && !serverRead[id]) postState("/api/read", { id: Number(id), read: true });
|
||||
});
|
||||
// Migrate device-local reads the account doesn't have yet — but only
|
||||
// once per device. After the first sync the server is authoritative, so
|
||||
// a story the user later marks unread on another device stays unread
|
||||
// instead of being perpetually resurrected from this device's stale
|
||||
// local set on every page load.
|
||||
var MIGRATED_KEY = "pete.readMigrated.v1";
|
||||
var migrated = false;
|
||||
try { migrated = localStorage.getItem(MIGRATED_KEY) === "1"; } catch (e) {}
|
||||
if (!migrated) {
|
||||
ids.forEach(function (id) {
|
||||
if (readSet[id] && !serverRead[id]) postState("/api/read", { id: Number(id), read: true });
|
||||
});
|
||||
try { localStorage.setItem(MIGRATED_KEY, "1"); } catch (e) {}
|
||||
}
|
||||
saveRead(readSet);
|
||||
})
|
||||
.catch(function () {});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//
|
||||
// Bump CACHE_VERSION whenever the precached shell assets change; activate()
|
||||
// drops every cache that doesn't match the current version.
|
||||
var CACHE_VERSION = "v1";
|
||||
var CACHE_VERSION = "v2";
|
||||
var SHELL_CACHE = "pete-shell-" + CACHE_VERSION;
|
||||
var RUNTIME_CACHE = "pete-runtime-" + CACHE_VERSION;
|
||||
|
||||
@@ -132,23 +132,18 @@ self.addEventListener("fetch", function (event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Page navigations: network-first, fall back to a cached copy of the same page,
|
||||
// then to the offline card. Successful HTML is cached so revisits work offline.
|
||||
// Page navigations: network-only, falling back to the offline card when the
|
||||
// network is unreachable. We deliberately do NOT cache HTML responses: pages
|
||||
// are personalized (they embed the signed-in user's name/email and a "For you"
|
||||
// rail), and the runtime cache is shared across everyone who uses this
|
||||
// installed PWA. Caching a navigation would let a signed-out visitor — or a
|
||||
// second person on the same device — be served the previous user's identity
|
||||
// and personalized stories offline. Offline reading still works: the reader
|
||||
// fetches cached /api/article JSON on top of the cached static shell.
|
||||
if (req.mode === "navigate") {
|
||||
event.respondWith(
|
||||
fetch(req).then(function (res) {
|
||||
if (res && res.ok) {
|
||||
var copy = res.clone();
|
||||
caches.open(RUNTIME_CACHE).then(function (cache) {
|
||||
cache.put(req, copy);
|
||||
trimCache(RUNTIME_CACHE, RUNTIME_MAX);
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}).catch(function () {
|
||||
return caches.match(req).then(function (hit) {
|
||||
return hit || offlineFallback();
|
||||
});
|
||||
fetch(req).catch(function () {
|
||||
return offlineFallback();
|
||||
})
|
||||
);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user