Skip to content

Security rules

Every change to nepwalk-be or nepwalk-fe follows these rules. They apply to people and AI assistants alike. Check them before opening a pull request; the PR template asks for it.

Each rule has an ID. Quote the ID in reviews and commit messages (for example "S3") so everyone knows which rule is meant.

Why this exists: NepWalk holds real travellers' names, phones, flights and group leaders' contacts. The 23 Sep 2026 code review found gaps that no test caught (SEC-01–SEC-04 in tasks.md). As the code grows, a written list is easier to keep than memory.

1. Access and privacy

  • S1 · One read rule. Every endpoint that returns trip data (trips, days, stops, templates, share links) checks access with canViewTrip / isTripInsider in src/modules/trips/trip-access.ts. No endpoint writes its own access check.
  • S2 · Hide what isn't yours. A trip the viewer may not see returns 404, not 403, so private trips are not revealed.
  • S3 · Allowlist, never denylist. Anything an outsider sees (anonymous, signed-in non-member, share-link viewer) goes through an explicit list of public fields (publicTripView). Never send a whole database row and remove fields afterwards: new fields leak by default.
  • S4 · Never public: leader and member contacts, emails, invitation emails, flights, headcount, rooms, prices, commissions, vendor terms, tokens. This includes nested objects (a day's trip, a template's trip, a member's user).
  • S5 · Every ID is checked against its parent. A stop must belong to the day, the day to the trip, the trip to the viewer. Changing an ID in the URL must never reach another trip's data.
  • S6 · Organisation scope. Organisation trips are visible to that organisation's members only. Queries that list data filter by the viewer or the organisation; they never return "everything".
  • S7 · Share links are random, revocable, view-only tokens. They go through the same public projection as S3 and never grant write access.

2. Input

  • S8 · Every body, query and route parameter has a DTO or a pipe. No inline types like @Body() body: { isPublic: boolean }: they are not checked at runtime. IDs use ParseUUIDPipe or @IsUUID().
  • S9 · Validate what the client sent. The global pipe converts types before validating ("false" becomes true, 5 becomes "5"). Booleans use @StrictBoolean(); strings and IDs that must not be coerced use @KeepRaw() (src/common/validation/raw-body.ts).
  • S10 · Limits on every field. Strings have a maximum length; numbers have a range; arrays have a maximum size; free text is trimmed and blank values rejected.
  • S11 · Unknown fields are rejected (whitelist + forbidNonWhitelisted stay on). Never turn them off to make a request pass.
  • S12 · No raw SQL built from strings. Use Prisma queries. $queryRaw only with tagged templates; $queryRawUnsafe and $executeRawUnsafe are not allowed.

3. Output and sanitisation

  • S13 · Never render user content as HTML. No dangerouslySetInnerHTML with anything a user, vendor or AI wrote. React escapes text by default; keep it that way.
  • S14 · Links from data are checked. Photo and website URLs stored from input must be https:// (or http://); reject javascript: and data: URLs. External links open with rel="noopener noreferrer".
  • S15 · Messages built from data are encoded. WhatsApp (wa.me) and email links use encodeURIComponent on every value.

4. Login, tokens and sessions

  • S16 · No tokens in URLs. Access tokens, refresh tokens and ID tokens never go in a query string or path: URLs end up in logs, history and analytics (RFC 9700 §4.3.2). Return them in a response body. A one-time code in a URL is allowed only if it is short-lived, single-use and useless without a secret held by the browser that started the login (PKCE style).
  • S17 · Third-party logins are verified, not trusted. Google sign-in sends an ID token that the backend verifies: signature, audience (our client ID), issuer, expiry and verified email (T18). Any redirect-based OAuth flow uses a random state and PKCE tied to the browser that started it, checked before the code is exchanged.
  • S18 · Refresh tokens rotate. Each refresh issues a new token and the old one stops working. Logout clears both tokens on the client.
  • S19 · Secrets are required, never defaulted. JWT secrets and API keys come from the environment and the app refuses to start without them. No fallback values in code.
  • S20 · Nothing secret in the frontend bundle. Only non-secret values use NEXT_PUBLIC_. API keys (OpenRouter, Google client secret) stay on the backend.

5. Errors and logs

  • S21 · Clients get safe errors. Bad input returns 400 with a plain message. Unexpected failures return a generic 500 (GlobalExceptionFilter); no stack traces, queries, file paths or input echoed back.
  • S22 · Logs hold no secrets or private data. Never log tokens, passwords, authorisation headers, full request bodies, or traveller contacts. Log IDs, not people.

6. Data and infrastructure

  • S23 · CORS stays an allowlist (CORS_ORIGINS / FRONTEND_URL). Never reflect any origin with credentials.
  • S24 · No real traveller data before backups work. Nightly backup and a tested restore come first (Neon gate).
  • S25 · Migrations are additive and reviewed. No dropping columns or tables with live data without a written plan.
  • S26 · Dependencies are checked. Run npm audit before a release; critical and high advisories are fixed or recorded as an accepted risk in decisions.md.

7. AI features

  • S27 · Redact before sending. Names, phones, emails and passport numbers are removed before any text goes to an AI model.
  • S28 · AI output is data, not instructions. Validate it against a schema; never execute it, render it as HTML, or let it decide access, dates or money.

8. Tests every change needs

A change that touches any rule above is not done until these tests exist and pass:

  • New or changed read endpoint: tests as anonymous, as a signed-in outsider and as the owner. Fill every private field in the fixture and assert none of them appear in outsider responses (see test/trip-read-privacy.e2e-spec.ts).
  • New or changed write endpoint: a non-member gets 404; a member of another organisation gets 404; bad input gets 400 and nothing is saved (see test/safe-errors.e2e-spec.ts).
  • New request body: tests for a wrong type, a string "false" where a boolean is expected, a missing field and an unknown field.
  • Every guard is mutation-checked: remove the protection, see a test fail, restore it. A test that passes without the protection does not count.

9. Before opening a pull request

  1. Read the sections above that your change touches.
  2. Tick the security box in the PR template, naming the rules you checked (for example "S3, S8, S9").
  3. If a rule can't be met yet, say so in the PR and add the gap to tasks.md. Never merge silently around a rule.

Known gaps (25 Sep 2026)

Tracked in tasks.md → Review follow-ups. These must be closed before real traveller links are shared (SEC-02/03 fixed 25 Sep by Sign in with Google, T18):

  • S3: the share link (build day 14) must reuse publicTripView.
  • The frontend keeps tokens in localStorage. S13 matters doubly until they move to an HttpOnly cookie with a backend-for-frontend, when the NepWalk domain is live (October backlog, T18).