diff --git a/devdocs/modernization/NET10-PORT.md b/devdocs/modernization/NET10-PORT.md index 8dcca74c..d5ff33c5 100644 --- a/devdocs/modernization/NET10-PORT.md +++ b/devdocs/modernization/NET10-PORT.md @@ -193,7 +193,7 @@ John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own s | 4724 | SMTP: certificate checked, password never returned | F30, F31. **Done** (certificate checked, opt-out config setting `AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE` with manual page; password masked as `********` and kept on save; test `SmtpPasswordHidden`; self-signed listener proof; changelog lines for both) | | 4725 | Sessions: password change ends sessions, exact-path token exemption, default SuperUser refused when licensed | F7, F8, F9. **Done** (sessions end on reset, administrator password change and login disabled; exact route exemption, inactive users refused; default-password SuperUser limited to changing it on a purchased licence, option A; test `Sessions`; John's click-through with a temporary licence override; manual and changelog) | | 4726 | Login throttling/lockout (raven and sockeye) | F6. **Done** (10 failures per login or 50 per address in 15 minutes, then 429 for the rest of the window; same in Sockeye, whose login is public; test `LoginLockout`; per-address limit proved with a forwarded documentation address) | -| 4727 | Low severity hardening | F10, F11, F12, F22, F23, F25, F29, F35, F36, F38; the AyaNova half of F39 (DB ID not shown anonymously). In steps: 1 logo (F23) **done**; 2 API root page (F11, F39) **done**; 3 `server-info` (F29); 4 job owner and template export (F22); 5 `OverrideUserId` and export download (F25); 6 safe headers (F35); 7 remove `dist/docker` (F38) | +| 4727 | Low severity hardening | F10, F11, F12, F22, F23, F25, F29, F35, F36, F38; the AyaNova half of F39 (DB ID not shown anonymously). In steps: 1 logo (F23) **done**; 2 API root page (F11, F39) **done**; 3 `server-info` (F29) **done**; 4 job owner and template export (F22); 5 `OverrideUserId` and export download (F25); 6 safe headers (F35); 7 remove `dist/docker` (F38) | | 4681 | Sockeye: licence/trial request hardening (tagged sockeye, rockfish) | F40. **Done** (sockeye `cd3ebed`: rate limits, email check, cryptographic codes; rvf unchanged) | **From the port and earlier triage** diff --git a/devdocs/modernization/SECURITY-REVIEW.md b/devdocs/modernization/SECURITY-REVIEW.md index bc1c633f..80b8dc77 100644 --- a/devdocs/modernization/SECURITY-REVIEW.md +++ b/devdocs/modernization/SECURITY-REVIEW.md @@ -226,7 +226,7 @@ take the next free number; a fixed or accepted finding keeps its ID and changes | F11 | Low | **fixed in case 4727** | API root page shows licensee, version, licence terms (and DB ID when unlicensed) to anyone | | F12 | Low | confirmed | Password hashing: PBKDF2-SHA512 with 10,000 iterations | | 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 | +| F29 | Low | **fixed in case 4727** | `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 | @@ -638,6 +638,7 @@ server DB ID and the licence as JSON, to any login including customer contacts. page needs some of it; contacts need little or none. Whether the DB ID matters to Sockeye (licence fetch, trial requests) is checked in pass 11. Fix: return the DB ID and licence only to staff, or only to ops and admin roles. +**Fixed in case 4727, step 3 (2026-10-24):** `server-info` returns the DB ID and licence, and `global-biz-setting/client` the DB ID and licensee, only to Service and NotService users (SuperUser is NotService), the same rule the About form already used to show them; customer contacts and subcontractors get empty values. Test `ApiRoute.DbIdAndLicenceForStaffOnly` (a contact received the DB ID on the old code). ### Pass 7: routes without a role check that were found OK @@ -821,6 +822,7 @@ not secret: the anonymous API root page shows it while a server is unlicensed (F 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. +**AyaNova half fixed in case 4727, steps 2 and 3 (2026-10-24):** the DB ID is no longer shown anonymously (API root page) nor to customer contacts and subcontractors (`server-info`, client settings). The Sockeye half (repeatable fetch) is the later two-sided case with the server fingerprint. ### F40 Trial request routes (pass 11, from code) diff --git a/server/AyaNova/Controllers/ApiRootController.cs b/server/AyaNova/Controllers/ApiRootController.cs index 2dd79495..6036474c 100644 --- a/server/AyaNova/Controllers/ApiRootController.cs +++ b/server/AyaNova/Controllers/ApiRootController.cs @@ -119,6 +119,9 @@ namespace AyaNova.Api.Controllers [HttpGet("server-info")] public ActionResult ServerInfo() { + //case 4727 (F29): the DB ID and licence go to staff only (Service, NotService), as the About form shows them + var userType = UserTypeFromContext.Type(HttpContext.Items); + bool staff = userType == UserType.Service || userType == UserType.NotService; return Ok(new { data = new @@ -127,8 +130,8 @@ namespace AyaNova.Api.Controllers DBSchemaVersion = AySchema.currentSchema, ServerLocalTime = DateUtil.ServerDateTimeString(System.DateTime.UtcNow), ServerTimeZone = TimeZoneInfo.Local.Id, - ServerDbId = AyaNova.Core.License.ServerDbId, - License = AyaNova.Core.License.LicenseInfoAsJson, + ServerDbId = staff ? AyaNova.Core.License.ServerDbId : string.Empty, + License = staff ? AyaNova.Core.License.LicenseInfoAsJson : null, #if (SUBSCRIPTION_BUILD) Build = "Subscription" #else diff --git a/server/AyaNova/Controllers/GlobalBizSettingsController.cs b/server/AyaNova/Controllers/GlobalBizSettingsController.cs index c551d518..5492c75d 100644 --- a/server/AyaNova/Controllers/GlobalBizSettingsController.cs +++ b/server/AyaNova/Controllers/GlobalBizSettingsController.cs @@ -109,6 +109,10 @@ namespace AyaNova.Api.Controllers return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); } + //case 4727 (F29): the DB ID and licensee go to staff only (Service, NotService) + var userType = UserTypeFromContext.Type(HttpContext.Items); + bool staff = userType == UserType.Service || userType == UserType.NotService; + bool IsSubscriptionBuild = false; #if (SUBSCRIPTION_BUILD) @@ -136,8 +140,8 @@ namespace AyaNova.Api.Controllers //used to drive UI in case of unlicensed or attention required LicenseStatus = AyaNova.Core.License.ActiveKey.Status, MaintenanceExpired = AyaNova.Core.License.ActiveKey.MaintenanceExpired, - ServerDbId = AyaNova.Core.License.ServerDbId, - Company = AyaNova.Core.License.ActiveKey.RegisteredTo, + ServerDbId = staff ? AyaNova.Core.License.ServerDbId : string.Empty, + Company = staff ? AyaNova.Core.License.ActiveKey.RegisteredTo : string.Empty, SBuild = IsSubscriptionBuild, //used to drive UI in case of out of date release message to be shown to staff users non subscription only ShowUpdateAvailable = (Util.AyaNovaVersion.VersionString != AyaNova.Util.ServerGlobalOpsSettingsCache.LATEST_VERSION && !IsSubscriptionBuild),