diff --git a/devdocs/modernization/SECURITY-REVIEW.md b/devdocs/modernization/SECURITY-REVIEW.md index 83591219..3ff9570a 100644 --- a/devdocs/modernization/SECURITY-REVIEW.md +++ b/devdocs/modernization/SECURITY-REVIEW.md @@ -20,8 +20,8 @@ keep it that way while findings are open. | 7 | Staff roles: the authenticated routes without a role check in the action (list below) | Done 2026-10-23 | | 8 | Email and notifications (`direct-smtp`, templates, delivery) | Done 2026-10-23 | | 9 | Client (XSS, token storage, `npm audit` triage), CORS, headers, Swagger UI | Done 2026-10-23 | -| 10 | Deployment: installers, Linux setup script, nginx, systemd, config and data permissions | Next | -| 11 | Sockeye public routes (licence fetch, trial requests) | | +| 10 | Deployment: installers, Linux setup script, nginx, systemd, config and data permissions | Done 2026-10-23 | +| 11 | Sockeye public routes (licence fetch, trial requests) | Next | The order is by risk to internet-facing servers. Findings from any pass go in **Findings**. @@ -210,7 +210,9 @@ Status: *candidate* (seen in code, not yet confirmed), *confirmed*, *fixed in ca | F34 | Medium | confirmed | Shipped client libraries behind on security fixes: DOMPurify 2.3.8 (wiki sanitiser), Vuetify 2.6.6 | | F6 | Medium | confirmed | No lockout or rate limit on login or the 2FA step; no password rules | | F9 | Medium | confirmed | Default SuperUser password, and an anonymous route that says whether it is still set | -| F2 | Medium | candidate | Windows installers give all local users modify rights on the program and data folders | +| F36 | Medium | confirmed (code) | AyaNova listens on all interfaces on plain HTTP port 7575 in every template, hosted droplets included, and the setup script adds no firewall | +| F37 | Medium | confirmed (code) | Standalone PostgreSQL accepts any local connection without a password (`trust`) | +| F2 | Medium | confirmed (code) | Windows installers give all local users modify rights on the program and data folders, `config.json` included | | F7 | Low | confirmed | Password change or reset does not end existing sessions, and makes the download key non-expiring | | F8 | Low | confirmed | Latest-token check skipped for any URL ending in `/auth` or `notify/hello` | | F4 | Low | confirmed (code) | Any logged-in user can send an in-app notification to any user | @@ -220,6 +222,7 @@ Status: *candidate* (seen in code, not yet confirmed), *confirmed*, *fixed in ca | F25 | Low | confirmed (code) | Attachment and export gaps: upload stored before the rights check, "attached by" can be set to anyone, exports not tied to their user | | F29 | Low | confirmed (code) | `server-info` gives every login, customer contacts included, the server DB ID and full licence details | | F35 | Low | confirmed | No security headers from the server itself (self-hosted), no CSP anywhere, CORS any origin, public Swagger UI, download keys in URLs | +| F38 | Low | confirmed (code) | Linux hardening: no systemd sandboxing, service user can replace program files, desktop runs as root, unused Docker files with a fixed database password | | F22 | Low | confirmed | Minor report and job route gaps: template export by any download key, job status, logs and cancel by GUID without an owner check, browser without sandbox | **Release decisions.** John, 2026-10-23: **F13 to F17 are urgent and are fixed in 8.3.0**, as @@ -323,7 +326,7 @@ A new database has SuperUser `superuser` / `l3tm3in` (`PrimeData.cs`). Anonymous `GET notify/hello` returns `sudf: true` while that is still the password, and the login form then fills it in. Anyone can therefore check an internet-facing server for it in one request and log in as SuperUser. Each call also runs a PBKDF2 hash, a cheap way to load the server. -Whether hosted droplets set their own SuperUser password at setup: to check in pass 10. +Whether hosted droplets set their own SuperUser password at setup: to check in pass 10. **Pass 10:** they do not. The setup script generates the database password and JWT secret but not a SuperUser password, so every new hosted droplet starts internet-facing with `l3tm3in` until someone changes it. Fix in the script: generate one, set it once with `AYANOVA_SET_SUPERUSER_PW`, and write it to `/root/ayanova-secrets.txt` with the others. Fix options: only return `sudf` from local or LAN addresses, or force a password change on first SuperUser login; at least warn in the UI and the manual. @@ -709,6 +712,48 @@ Fix: add the headers in the app (so every deployment gets them), with a CSP that client's own scripts; limit CORS to the server's own origin unless API users need more; consider Swagger UI for logged-in users only. +### F36 Listening on all interfaces, no firewall (pass 10, from code) + +Every `config.json` template (`lan-install-config.json`, `linux-server`, `linux-desktop`, the +standalone launcher's) sets `AYANOVA_USE_URLS` to `http://*:7575`. On hosted droplets nginx +proxies HTTPS to `127.0.0.1:7575`, but Kestrel also listens on the public address, and the setup +script configures no host firewall. Unless a DigitalOcean cloud firewall blocks it (**John to +check**), anyone can reach AyaNova directly on port 7575 over plain HTTP, without TLS and +without nginx's headers, and logins sent there travel in clear. The standalone (single-PC) +install also accepts connections from the whole network. Fix: hosted and Linux server +templates bind `http://127.0.0.1:7575` when behind a proxy; the setup script enables `ufw` +allowing only 22, 80 and 443; standalone binds to localhost; LAN stays on all interfaces (that +is its purpose) and the manual recommends HTTPS through IIS for anything beyond the LAN. + +### F37 Standalone PostgreSQL trusts local connections (pass 10, from code) + +`dist/win-x64/postgres-empty-database/pg_hba.conf` uses `trust` for local, 127.0.0.1 and ::1, and +`listen_addresses` is the default (localhost). Any user or program on that PC can connect as any +database user, the `postgres` superuser included, without a password. Standalone is a +single-PC (mostly evaluation) install, so the exposure is local only; with F2 the database +files are writable by every local user anyway. Fix: `scram-sha-256` with a password generated +at install and written to the launcher's `config.json`; or accept it for evaluation installs +(John's decision). + +### F38 Linux hardening (pass 10, from code) + +- The systemd unit written by the setup script runs as `www-data` with no sandboxing options + (`NoNewPrivileges`, `ProtectSystem=strict` with `ReadWritePaths=/var/ayanova/data`, + `PrivateTmp`, `ProtectHome`). +- `chmod g+rw /var/ayanova/` gives `www-data` (which nginx also runs as) write access to the + program folder itself, so the service account can replace program files by renaming. Only + `data` needs write access. +- The Linux desktop install runs as root (`sudo ./AyaNova`, see Deferred), so the report browser, + already started without Chrome's sandbox, runs as root too. +- `dist/docker/linux-x64/docker-compose.yml` publishes PostgreSQL on port 5432 with a fixed + superuser password. It is not built (`build-release.bat` lines are commented out) or + documented; delete it or fix it before anyone uses it. + +OK in pass 10: the setup script generates a unique database password and JWT secret per +droplet, stores them in `/root/ayanova-secrets.txt` (600), makes `config.json` 640 `root:www-data`, +runs AyaNova as `www-data`, installs unattended upgrades, uses PGDG PostgreSQL on its default +localhost listener, and gets a Let's Encrypt certificate with HSTS and the headers in F35. + ### F2 Windows folder permissions (from the 8.3.0 installer test) `lan.iss` and `standalone.iss` set `Permissions: users-modify` on `{app}` (the program @@ -717,7 +762,7 @@ attachments, logs). Any local Windows user can therefore replace program files t the user who starts AyaNova or as the IIS application pool, and can read the database files and backups. Matters on shared or terminal-server machines. The program-folder permission originally let the report browser download beside the app; since 8.3.0 it downloads under -the data folder, so `{app}` may no longer need it. To confirm in pass 10. +the data folder, so `{app}` may no longer need it. To confirm in pass 10. **Pass 10:** confirmed from `lan.iss` and `standalone.iss`. `config.json`, with the database password and the JWT secret, is installed in `{app}`, so any local user can read those secrets and change the configuration or the program files; standalone also grants `users-modify` on `{app}\local-postgres` (the PostgreSQL binaries) and the database folder. Fix: drop `users-modify` from `{app}` and `local-postgres`; keep write access only where the running account needs it (logs, backups, temp, attachments, report browser), granted to that account or to Administrators rather than all Users; give `config.json` an ACL for Administrators and the service account. ### F3 `notify/direct-smtp` open to every logged-in user @@ -788,3 +833,4 @@ roles that should not see the schedule (pass 7). | 2026-10-23 | 7 Staff roles | Went through the remaining logged-in routes without a role check, the restricted-role filtering in `WorkOrderBiz`, and user and contact administration in `UserBiz`. Tested on the local dev server, read-only: **F27 confirmed** (`SubContractorRestricted` through `report/data`). F28 from code (live test not done). New: F29. The rest of the list found OK (see "Pass 7: routes ... found OK") | | 2026-10-23 | 8 Email and notifications | Code reading only, nothing sent. Read `NotifyController` (direct SMTP and message), `Mailer`, `GlobalOpsNotificationSettings` controller, biz and model, `NotifyEventHelper`, `CoreJobNotify`, `CoreJobCustomerNotify`, `NotifySubscriptionBiz`. F3 and F4 confirmed by code; F17 confirmed and re-rated Medium (names and links, no reports). New: F30, F31, F32. **Action for John: rotate the FastMail app password in F32** | | 2026-10-23 | 9 Client, headers, CORS, Swagger | Code reading of every `v-html` use in `raven-client` and its data source; `npm audit` read-only against the lockfile (no install, no change), triaged to shipped packages; server and nginx headers, CORS, Swagger. New: F33, F34 (needs John's decision on two front-end packages), F35; F14 extended (`unit/customerunit`) | +| 2026-10-23 | 10 Deployment | Code reading: `lan.iss`, `standalone.iss`, the four `config.json` templates, the standalone PostgreSQL configuration, the hosted setup script in `sockeye-client` (secrets, permissions, systemd unit, nginx, certbot), `dist/docker`. F2 confirmed and extended (`config.json` readable by all users); F9 extended (hosted droplets start with the default SuperUser password). New: F36, F37, F38. **John to check:** whether a DigitalOcean cloud firewall blocks port 7575 on hosted droplets |