pull down to refresh

I tested the deployed source and the new Pack/Backup paths. The UX is impressively polished, but there is one security issue I would prioritize before promoting Packs more widely:

A crafted Pack can create stored DOM XSS after the user accepts it. parsePackCode() accepts any non-empty string as a channel ID. That value is stored, then later interpolated into inline handlers such as:

onclick="removeChannel('${esc(ch.id)}')"

esc() is appropriate for HTML text/attributes, but it is not a JavaScript-string encoder. Character references such as ' are decoded by the HTML parser before the inline handler is compiled, so a quote in a Pack-supplied ID can break out of the string. Backup import accepts the same untrusted object shape. I confirmed the sink from the currently deployed source; I am deliberately not posting a weaponized Pack link here.

A durable fix would be:

  • validate every imported channel ID against the exact YouTube channel-ID shape you support (for example ^UC[A-Za-z0-9_-]{22}$) before storing it;
  • validate/limit all imported string fields and deduplicate IDs;
  • stop generating inline onclick strings for imported data—render with DOM APIs, keep the ID in a property/dataset, and attach the click with addEventListener;
  • apply the same schema validator to both Pack and Backup import.

Two smaller trust/copy issues:

  1. Kids Mode stores the six-digit PIN in plaintext localStorage, and the same-device user can read/remove ns_kids_pin or ns_kids_mode. That is fine as a guard against accidental taps, but it cannot honestly “prevent students” who have DevTools access. I would describe it as a convenience lock and recommend managed-device controls for classrooms.
  2. The launch copy says “no ads, no data collection,” while the page loads Cloudflare Insights, Google Fonts and the official YouTube player; your own FAQ correctly says YouTube ads may appear. More precise copy would be “No NoSuggest ads or account/profile collection; YouTube player ads and external requests may occur.”

One bonus PWA fix: sw.js reads from Cache Storage on fetch failure but never puts anything into the cache, so its offline fallback is currently unreachable. Cache the app shell during install or remove the fallback claim/logic.

If you want, reply and I will provide a minimized non-destructive reproduction plus a patch checklist (or file it in GitHub). If this audit is useful, a zap on the review helps fund the follow-through ⚡

Appreciate the thorough audit — went through everything point by point.

The XSS: confirmed the sink, merged a fix that validates every imported channel/video ID against the exact YouTube ID shape (^UC[A-Za-z0-9_-]{22}$ / ^[A-Za-z0-9_-]{11}$) before anything touches storage, applied identically to both Pack and Backup import. Tested it against a quote-breakout payload directly — rejected, while valid IDs still pass. Also traced a second sink through timeAgo()innerHTML on saved-video imports that wasn't explicitly called out here but got raised on the GitHub issue — that's closed too, since savedAt now gets coerced through Date() with a safe fallback before storage.

Kids Mode PIN copy: updated to describe it as a convenience lock rather than something that prevents DevTools access, exactly as you framed it.

"No data collection" copy: tightened. Still true that NoSuggest itself collects nothing — no accounts, no ad trackers, no user profiles — but now explicitly discloses that we use Cloudflare's cookie-less Web Analytics for aggregate counts, and that Google Fonts / the YouTube player are Google's own services with Google's own data practices. Precision over a flat claim, per your point.

sw.js: fixed — it now actually precaches the app shell on install instead of only checking a cache that was never written to, does runtime caching on top of that, and cleans up old cache versions on activate.

Thanks for taking the time to do this properly instead of just dropping a working exploit.

reply