Skip to content

Security: niels-emmer/time-keeper

Security

SECURITY.md

Security

This document is aimed at anyone considering self-hosting this project. It gives an honest overview of the security posture, known risks, and mitigations so you can make an informed decision.


Who wrote this code?

All application code was generated by Claude Code (Anthropic). The author directed the design and reviewed the output, but the code has not been independently audited or pentested. Treat it accordingly.


Threat model

This is a single-user (or small-team) personal productivity tool, not a public-facing service. The realistic threats are:

Threat Likelihood Mitigation
Unauthorised access to your time data via browser Low Authentik auth blocks the app entirely before any request reaches it
Unauthorised access via API token Low 256-bit tokens; HTTPS enforced on api.* subdomain; rate-limited; token never stored in plain text
API token brute-forced Negligible 2^256 keyspace; rate limit (120 req/min per IP) further constrains attempts
Bearer token stolen from device Low Stored in macOS Keychain (not plaintext); revoke immediately from web app Settings
Token used to create more tokens None Token management endpoints require Authentik header auth — Bearer tokens receive 403
Unauthorised modification of another user's time entries via adjust-cell None PATCH /api/summary/adjust-cell validates category ownership before any write; all DB operations are scoped to req.userId
SQL injection Low All queries use Drizzle ORM with parameterised statements; no raw SQL with user input
XSS Low React escapes output by default; no dangerouslySetInnerHTML is used
Supply chain attack on a dependency Very low Standard npm ecosystem risk; see dependency audit below
Server-side request forgery N/A The backend makes no outbound HTTP requests
Secrets in the repo None No API keys, tokens, or credentials are stored in the codebase

Authentication

The application supports two auth paths:

Path 1 — Browser / PWA (Authentik forward auth)

Internet → NPM (TLS + Authentik forward auth) → frontend nginx → backend

The backend reads the X-authentik-email header set by the Authentik outpost. If absent in production, the backend returns 401. Auth is entirely delegated to Authentik — there is no second line of defence if forward auth is misconfigured.

See docs/integration/auth.md.

Path 2 — Native app (Bearer token via api.* subdomain)

Internet → NPM (TLS only, no forward auth) → backend:38522 → Bearer token check

Added to support the macOS status bar app. The api.* subdomain bypasses Authentik but the backend validates the Authorization: Bearer <token> header itself:

  • Tokens are 32 random bytes encoded as base64url (256-bit entropy)
  • Only the SHA-256 hash is stored in the database — a DB breach does not expose usable tokens
  • Tokens are shown once on creation and cannot be retrieved again
  • Tokens are revocable from the web app (Settings → Personal Access Tokens)
  • Token management endpoints require Authentik-header auth — a Bearer token cannot create or list tokens
  • The backend port (38522) is bound to 127.0.0.1 — not reachable from other LAN devices
  • HTTPS is enforced by NPM on the api.* subdomain

See docs/integration/api-subdomain.md.


Data storage

  • All data lives in a single SQLite file on the host (/data/time-keeper.db inside the backend container, mounted as a Docker volume).
  • There is no external database, no cloud sync, no analytics, no telemetry.
  • The file is readable by anyone with shell access to the host. Protect it with standard filesystem permissions and volume encryption if required.
  • Backup = copy the file. See docs/integration/docker.md for the one-liner.

Dependency audit

Audited with yarn npm audit on 2026-04-07: no vulnerabilities found.

vitest and jsdom were added in v0.10.0 as dev-only dependencies (not shipped in the production Docker image). They are omitted from the runtime table below.

Runtime dependencies and their risk profiles:

Package Version pinned to Notes
express ^4 15+ years old, very widely used, actively maintained
better-sqlite3 ^11 Widely used SQLite binding; includes a native C++ addon compiled at install time — highest-risk category by nature of native code
drizzle-orm ^0.38 Newer library (2022–); less battle-tested than Prisma but growing adoption
zod ^3 Pure TypeScript; used for all input validation on the backend
express-rate-limit ^7 Simple, auditable rate-limiting middleware
react + vite current Mainstream, heavily used
@tanstack/react-query current Mainstream, well-maintained
@radix-ui/* / shadcn/ui current Accessible, widely used UI primitives
@dnd-kit/* current Pure JS drag-and-drop; no native code

To re-run the audit yourself:

yarn npm audit

The native addon caveat: better-sqlite3 compiles C++ at yarn install time using node-gyp. This is normal and the package is widely used, but it means you are trusting both the npm registry and your local C++ toolchain. If this concerns you, inspect node_modules/better-sqlite3/build/ after install.


What has not been done

To be explicit about what a proper security review would include and this project has not had:

  • No penetration test
  • No static analysis (SAST) beyond TypeScript's type checker
  • No dependency licence audit
  • No review of the nginx configuration for header injection or bypass
  • No assessment of the Authentik configuration for misconfigurations
  • No fuzzing of API inputs beyond Zod schema validation

Reporting issues

This is a personal project with no formal security team. If you find a vulnerability:

  1. Open a GitHub issue and label it security
  2. Or contact the repository owner directly via GitHub

There is no SLA or bug bounty.

There aren’t any published security advisories