From d6a7e024f04f7a474bddf5930b977e1bbc53e76a Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Fri, 23 Oct 2026 20:51:03 -0700 Subject: [PATCH] Security review pass 11: Sockeye public routes; all passes done From code: the 2FA flaw (F5) is also in Sockeye and the business server (F41, critical; mitigation: turn 2FA off until fixed); the licence-signing private keys are hardcoded in Sockeye's source (F42); one-shot licence fetch by DB ID (F39); trial request routes without rate limits (F40). Review only. Co-Authored-By: Claude Opus 5.5 --- devdocs/modernization/SECURITY-REVIEW.md | 58 +++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/devdocs/modernization/SECURITY-REVIEW.md b/devdocs/modernization/SECURITY-REVIEW.md index 16b3bc91..b05824e2 100644 --- a/devdocs/modernization/SECURITY-REVIEW.md +++ b/devdocs/modernization/SECURITY-REVIEW.md @@ -21,7 +21,7 @@ keep it that way while findings are open. | 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 | Done 2026-10-23 | -| 11 | Sockeye public routes (licence fetch, trial requests) | Next | +| 11 | Sockeye public routes (licence fetch, trial requests) | Done 2026-10-23 | The order is by risk to internet-facing servers. Findings from any pass go in **Findings**. @@ -186,6 +186,8 @@ Status: *candidate* (seen in code, not yet confirmed), *confirmed*, *fixed in ca | ID | Severity | Status | Title | |---|---|---|---| +| F41 | **Critical** | confirmed (code) | F5 also in Sockeye (internet-facing admin login) and in the 8.2.x business AyaNova server | +| F42 | High | confirmed (code) | The licence-signing RSA private keys are hardcoded in Sockeye's source and binaries | | F5 | **Critical** | **confirmed** | 2FA login step accepts an empty or missing temp token: the password is skipped for users with 2FA on | | F1 | Critical (installs that kept it) | **confirmed** | Placeholder JWT secret in the LAN/IIS and Linux server config templates | | F19 | Critical | **confirmed** (import) | Any logged-in user, customer contacts included, can import report templates, whose JavaScript runs in the server's browser | @@ -223,6 +225,8 @@ Status: *candidate* (seen in code, not yet confirmed), *confirmed*, *fixed in ca | 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 | +| F39 | Low | confirmed (code) | Licence fetch is one-shot by DB ID, and the DB ID is shown anonymously while unlicensed: someone else can take a new key first | +| F40 | Low | confirmed (code) | Trial request routes: no rate limit, non-cryptographic verification codes, a fixed bot-check value | | 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 @@ -757,6 +761,57 @@ droplet, stores them in `/root/ayanova-secrets.txt` (600), makes `config.json` 6 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. +### F41 The 2FA flaw (F5) in Sockeye and the business server (pass 11, from code) + +Sockeye is a near-copy of this codebase: `sockeye/server/Controllers/AuthController.cs:168` matches +the 2FA step on `TempToken` alone and sets it to `string.Empty` after success, exactly as F5. +Sockeye's login is on the internet (sockeye.ayanova.com) and holds the customer list, licences +and subscription servers; the business AyaNova server (8.2.x) has the same code. If an account on +either has 2FA turned on, it can be reached with no password and a guessed code. **Immediate +mitigation (no code change):** turn 2FA off on those accounts until the fix is deployed; with 2FA +off the account is not matched by this query and the password protects it as usual. Then apply +the F5 fix to Sockeye too. Other shared-code findings apply to Sockeye as well (F7, F8, F12, F19, +F21, F30; F1 if its config kept a placeholder); its admin UI is used by John alone, so only the +anonymous ones (F5, F41) are urgent there. + +### F42 Licence-signing private keys in Sockeye source (pass 11, from code) + +`sockeye/server/biz/RavenKeyFactory.cs` holds two RSA private keys as string literals (lines 158 +and 222; not reproduced here) and signs licence keys with them. They are therefore in the Sockeye +repository and its history (self-hosted Gitea), in every build of Sockeye, and on the Sockeye +droplet in the deployed DLL. Anyone who obtains any of those can create licence keys that every +AyaNova server accepts. AyaNova checks licences against the matching public key +(`License.cs`, "our real 2016 public key"). Fix: load the private key at run time from a file or +environment variable on the Sockeye server only (root-readable, outside the repo and the +published files) and remove it from the source. Rotation (new key pair, new public key in +AyaNova, every licence reissued) is a business decision for John; without rotation the keys stay +in history, so access to the repository and its backups must be treated as access to the keys. + +### F39 One-shot licence fetch by DB ID (pass 11, from code) + +`RvfController` (`POST rvf`, anonymous) returns the newest active licence for a DB ID, once: +`FetchedOn` is set on the first fetch and later fetches get 404. DB IDs are random (32 bytes) but +not secret: the anonymous API root page shows it while a server is unlicensed (F11) and +`server-info` shows it to every login (F29). Someone who knows it can take a newly issued key +first; the real server then never receives it (a support case; the key only works for that DB +ID, but it names the licensee and features). Fix: let the same DB ID fetch its key again (the key +is bound to the DB ID, so one-shot adds nothing), and stop showing the DB ID anonymously. + +### F40 Trial request routes (pass 11, from code) + +`POST rvr` (AyaNova trial request) and `POST subscription-server-trial-request` are anonymous with no +rate limit. Each `rvr` request with a new DB ID stores a request and sends Sockeye's fixed +verification email to the address given; the email confirm code (`StringUtil.GenFetchCode`) is +10 letters from `System.Random`, not a cryptographic generator. The subscription-server request's +only bot check is a fixed value (`RValue == "totallylegit"`). Approval is manual, so none of this +issues a licence; the exposure is email to arbitrary addresses and a cluttered request list. +Fix: nginx `limit_req` on these routes; `RandomNumberGenerator` for codes; optionally a CAPTCHA +on the website forms. + +OK in pass 11: no licence is issued without John's approval; a DB ID with a purchased licence +cannot request a trial; licence keys are bound to the DB ID and signed (AyaNova verifies the +signature). + ### F2 Windows folder permissions (from the 8.3.0 installer test) `lan.iss` and `standalone.iss` set `Permissions: users-modify` on `{app}` (the program @@ -837,3 +892,4 @@ roles that should not see the schedule (pass 7). | 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 then confirmed hosted droplets have a restrictive cloud firewall (F36 re-rated Low), accepted F37, and explained the trial default password (F9 note added) | +| 2026-10-23 | 11 Sockeye public routes | Code reading of `RvfController` (licence fetch), `RvrController` and `SubscriptionServerTrialRequestController` (trial requests), `TrialLicenseRequestBiz`, the code generator, Sockeye's `AuthController` and token middleware, and where licences are signed. New: **F41** (F5 in Sockeye and the business server; turn 2FA off until fixed), **F42** (signing private keys in source), F39, F40. **All 11 passes done** |