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 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 20:51:03 -07:00
parent 234d96aaa6
commit d6a7e024f0

View File

@@ -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** |