case 4725 - sessions end on password reset and login disabled, exact login route, default SuperUser limited when licensed

A password set by someone else (reset code, administrator) or a disabled
login replaces the user's session token and download key; changing one's
own password keeps the session. PasswordResetCodeExpire is cleared (was
DlKeyExpire). The token middleware exempts only the exact login and
notify/hello routes and refuses inactive users and users who may not log
in. With a purchased licence, notify/hello no longer offers the default
SuperUser login and a SuperUser with the default password may only change
it (SuperUserDefaultPasswordRoutes). F7, F8 and F9 fixed. Manual and
changelog updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 19:54:03 -07:00
parent 4141217c04
commit 80f81aea0f
12 changed files with 123 additions and 22 deletions

View File

@@ -215,12 +215,12 @@ take the next free number; a fixed or accepted finding keeps its ID and changes
| F32 | Medium | confirmed (code) | A real-looking SMTP account and password are in the DEBUG defaults in `raven` and `sockeye` source |
| F34 | Medium | **fixed in case 4722** | 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 (by design for trials), and an anonymous route that says whether it is still set |
| F9 | Medium | **fixed in case 4725** | Default SuperUser password (by design for trials), and an anonymous route that says whether it is still set |
| F36 | Low | confirmed (code) | AyaNova listens on all interfaces on plain HTTP port 7575 in every template; hosted droplets are protected by a cloud firewall |
| F37 | Low | accepted (John) | 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` |
| F7 | Low | **fixed in case 4725** | Password change or reset does not end existing sessions, and makes the download key non-expiring |
| F8 | Low | **fixed in case 4725** | Latest-token check skipped for any URL ending in `/auth` or `notify/hello` |
| F4 | Low | **fixed in cases 4712 and 4723** | Any logged-in user can send an in-app notification to any user |
| F10 | Low | confirmed | Anyone can record acceptance of the licence agreement (`license/lc`) |
| F11 | Low | confirmed | API root page shows licensee, version, licence terms (and DB ID when unlicensed) to anyone |
@@ -314,7 +314,7 @@ owner changes the password. It also sets `DlKeyExpire = null`, apparently meant
`PasswordResetCodeExpire`; with a null expiry `ValidateDownloadTokenAndReturnUserAsync`
never treats the key as expired, until the next login replaces it. Setting `AllowLogin` to
false does not end a session either (only deactivating does). Fix: on password change, set
`CurrentAuthToken` and `DlKey` to new random values and clear `PasswordResetCodeExpire`.
`CurrentAuthToken` and `DlKey` to new random values and clear `PasswordResetCodeExpire`. **Fixed in case 4725 (2026-10-24):** a password set by someone else (reset code, administrator on the user form) and unticking Allow login replace `CurrentAuthToken` and `DlKey` (key expired); changing one's own password keeps the caller's session, which is the only valid one anyway (one session per user). `PasswordResetCodeExpire` is cleared instead of `DlKeyExpire`. Test `Sessions` (fails on the old code).
### F8 Latest-token check bypassed by the URL suffix (pass 2, confirmed)
@@ -326,7 +326,7 @@ on `form-field-reference/Customer` and passed the middleware on `form-field-refe
The middleware also does not check `Active`. Impact is small on its own (only those routes,
with the user's current roles), larger with F1. Fix: compare the exact route
(`/api/v8/auth`, `/api/v8/notify/hello`), or skip the check by endpoint metadata
(`[AllowAnonymous]`) instead of by path.
(`[AllowAnonymous]`) instead of by path. **Fixed in case 4725 (2026-10-24):** the exemption compares the matched route exactly (`POST auth`, `GET notify/hello`, through `CustomerContactRoutes.RouteKey`); the middleware also refuses inactive users and users who may not log in. Test `Sessions.ReplacedTokenRefusedOnRouteEndingInAuth` (old code: passed the middleware, 404 from the action).
### F9 Default SuperUser password, advertised anonymously (pass 2)
@@ -338,6 +338,8 @@ Whether hosted droplets set their own SuperUser password at setup: to check in p
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.
**Fixed in case 4725 (2026-10-24), John's option A:** with a purchased licence (active or expired), `notify/hello` no longer returns `sudf`, and a SuperUser session with the default credentials may only use the routes the change password page needs (`SuperUserDefaultPasswordRoutes`, checked in the token middleware; the default-credentials result is kept per stored hash, so no extra hashing per request). Trial and unlicensed servers unchanged, as designed. Proved locally with a temporary, uncommitted purchased-licence override: other screens 403 until the password was changed, then normal (John's click-through). Hosted setup script still does not set a SuperUser password: accepted for trial mode.
### F10 Anonymous licence-agreement consent (pass 2)
`POST license/lc` with body `"iaccepttheagreement"` records the EULA consent; the comment says