The two things worth attacking here are: (1) someone else's FCM token / device list (lets an
attacker spam push notifications to a stranger's phone), and (2) someone else's link history
(privacy leak of browsing activity). Both are mitigated by the same rule: every query is scoped
to req.uid derived from a verified Firebase ID token, never from a client-supplied uid field.
- Firebase ID tokens verified server-side with
firebase-admin/authverifyIdToken()— never trust auidfield in a request body; always use the uid decoded from the verified token. - Token verification happens once, in
auth.middleware, before any route handler runs. - Clock-skew tolerant but still rejects tokens whose
exphas passed — no manual JWT decoding, always the Admin SDK's verifier (handles key rotation automatically). -
chrome.identityis used for sign-in so no password/credential ever touches our server.
-
deviceId/shareId/notificationIdownership is checked againstreq.uidin the controller before any read/write/delete — a syntactically valid ID belonging to another user returns403/404(404 specifically when revealing existence would itself be a leak, e.g. device lookups — see API_SPEC). - No endpoint accepts a
uidin the request body that is then trusted.
- Every route validates
body/query/paramswith azodschema invalidate.middlewarebefore the controller sees the request — malformed input never reaches business logic. - URL fields are validated as real
http(s)://URLs with a max length, not just "non-empty string", to stopjavascript:/data:URL QR-and-share abuse. - String length caps everywhere (
title≤ 300,label≤ 60,fcmToken≤ 4096) to bound Firestore document size and prevent storage-cost abuse.
-
fcmTokenis write-only from the client's perspective — no endpoint ever returns it in a response body, including the registration endpoint that just received it. Only the backend Admin SDK reads it (to call FCM). - Firebase service account key lives in a Vercel encrypted environment variable
(
FIREBASE_SERVICE_ACCOUNT_JSON, base64-encoded), never committed to the repo..envand*.serviceaccount.jsonare in.gitignore. - CORS allow-list is the extension's own origin (
chrome-extension://<id>) plus, during local dev,http://localhost:*— never*.
-
helmet()applied globally for standard security headers (HSTS,X-Content-Type-Options, noX-Powered-By, etc.). - All traffic is HTTPS-only (Vercel default); the manifest's host permission is an
https://origin, neverhttp://.
- Per-uid + per-IP rate limiting on
POST /api/share(e.g. 30/min) andPOST /api/device/register(e.g. 10/min) — generous enough for real usage, tight enough to stop a runaway loop or malicious script from FCM-bombing a device. - Backend-side duplicate detection (10s window on
uid + urlHash) is authoritative — independent of and in addition to any client-side debounce, which is purely cosmetic. - Dead/
NotRegisteredFCM tokens are markedisActive: falseon first failure rather than retried forever — stops wasted FCM calls and alerts the user to re-pair the device.
- Default-deny; every collection has an explicit owner-scoped rule (see
docs/FIRESTORE_SCHEMA.mdfor the summary,backend/firestore.rulesfor the real file). -
notificationsandshare_history(top-level) are not client-readable at all — the extension only ever sees its own data through the backend API, which applies extra business logic (pagination, search, redaction) that raw Firestore Rules can't express.
- A global Express error handler (
errorHandler.middleware) catches everything, logs with a request id, and returns a genericINTERNAL_ERRORto the client — stack traces and internal error messages never reach the response body. - No PII (raw URLs, emails) in log lines at
infolevel in production — only atdebug, which is off by default.
-
manifest.jsonrequests the minimum permission set (activeTab,storage,identity, one explicit host permission for the API origin) — see ARCHITECTURE.md §5 for the full justification table. - Content Security Policy in the manifest disallows remote code execution
(
script-src 'self'); Tailwind/FontAwesome are loaded from CDN as stylesheets, not scripts that execute in the extension's privileged context. -
popup.htmlnever useseval/innerHTMLwith untrusted (page-supplied) content — page title is inserted viatextContent, not HTML, to avoid script injection from a malicious page's<title>.
- No anti-abuse CAPTCHA — login is gated by Google's own bot detection on Sign-In, which is the appropriate layer for that concern.
- No end-to-end encryption of share payloads — title/URL/favicon are already visible to whoever visited the page; the security property we actually need is "only the intended device(s) receive it," not confidentiality from Google/Firebase infrastructure itself.