Skip to content

Security: dinooo13/habit-tracker

Security

SECURITY.md

Security

This document describes the security model of the Atomic Habit Tracker, its known limitations, and what would be required before any production deployment. It summarizes the full review in issue #1 (OWASP Top 10, 2021).

Security model

The app is a client-only Nuxt SPA (ssr: false) with no backend:

  • All data — habits, entries, coaching suggestions, settings — lives in the browser's IndexedDB, stored unencrypted, on a single device.
  • There is no server, no API, and no network transmission of user data.
  • This eliminates entire classes of server-side vulnerability, but shifts the risk to the client and to whoever can access the browser/device.

Known limitations & non-goals

These are deliberate trade-offs for a local-first demo app, not bugs to be surprised by:

  • The auth gate is not a security boundary. Authentication is a single localStorage flag (app/utils/auth/dummy-auth.ts) checked by client-side route middleware (app/middleware/auth.global.ts). Anyone with the browser, devtools, or any script on the same origin can bypass it. It exists for UX, not protection. See ADR-0007.
  • Data is unencrypted at rest and readable by any code on the same origin or anyone with device access.
  • No multi-user isolation. All data shares one namespace; logout does not clear data, so a shared device exposes the previous user's data.
  • Reminders/notifications are best-effort and depend on granted permission and the app being open. See ADR-0008.

Before production

If this app (or a fork) is ever deployed for real users, the review flagged these as the priority hardening items — tracked in issue #1:

  • Real authentication (an identity provider with server-side session validation) — or remove the auth gate entirely rather than imply protection (SEC-01, SEC-02).
  • Consider encrypting data at rest and clearing data on logout for shared devices (SEC-05, SEC-13).

Addressed hardening

Findings from the review that have since been mitigated within the local-first model:

  • Bounded import size and collection counts (SEC-06, SEC-09). Imported JSON is now bounded before it can freeze the tab or exhaust storage: a 64 MiB pre-read File.size check rejects an over-large file before it is read, and Zod .max() constraints plus a cheap raw-count preflight cap the habit/entry/suggestion collections and each habit's scheduleWeekdays/pauses arrays. String-field max-lengths (FIELD_LIMITS) and calendar date bounds already shipped; together these keep a crafted or accidental payload from driving unbounded work or storage. An over-limit import is rejected as a whole — never partially merged or truncated — and current data is left unchanged. See app/types/app-data.ts, app/utils/persistence/storage-schema.ts, and app/pages/app/settings.vue.
  • Dev-only tooling gated out of production builds (SEC-10). Nuxt DevTools (devtools: { enabled: isDev }) and the PWA devOptions are both keyed to NODE_ENV !== 'production', so nuxt build / nuxt generate never ship them. See nuxt.config.ts.
  • HTTP security headers (SEC-11). The generated .htaccess sets Content-Security-Policy (self-origin scripts/styles/images/fonts/connect/worker/manifest, object-src 'none', frame-ancestors 'none'; 'unsafe-inline' is still required for Nuxt/Vite's inline bootstrap and Nuxt UI's injected styles), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, and HSTS. The file is rendered from public/.htaccess.tpl into .output/public/ by the nitro:init close hook in nuxt.config.ts and mirrored to the host by deploy-production. Apache mod_headers must be enabled on the host for the headers to take effect.
  • Session timeout (SEC-03). The dummy-auth session now has an absolute 7-day expiry stamp; expired/malformed sessions read as logged-out and clear their stale keys. See ADR-0011. (This is hygiene, not access control — the gate is still bypassable by design.)
  • Service-worker update consent (SEC-14). registerType is now 'prompt': new workers download but only activate after the user confirms via a reload banner. See ADR-0008.
  • Security event logging (SEC-16). A lightweight, in-memory client-side event log (app/utils/observability/security-log.ts) records auth, import/export, deletion, validation-failure, and storage events to a bounded ring buffer + console. No network, no persistence.
  • Storage-quota / write-failure notice (SEC-18). Persistence write failures (especially QuotaExceededError) and a best-effort navigator.storage.estimate() pre-check now surface a user-facing warning toast instead of only console.error (app/composables/use-storage-health.ts).

Reporting a vulnerability

This is a personal/educational project. To report a security concern, open a GitHub issue labelled type: security (or contact the maintainer directly for anything sensitive). Please describe the issue, affected files, and reproduction steps.

There aren't any published security advisories