case 4727 - attached-by override for admins only, export download by its owner only
OverrideUserId on attachment upload is used only from SuperUser or a user with change rights on User, otherwise ignored. Export files carry the user's id in their name and export/download serves only the key user's own export files. F25 (4727 step 5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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) |
|
| 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) |
|
| 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) |
|
| 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) **done**; 4 template export (F22) **done**, job owner check accepted; 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 template export (F22) **done**, job owner check accepted; 5 `OverrideUserId` and export download (F25) **done**; 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) |
|
| 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**
|
**From the port and earlier triage**
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ take the next free number; a fixed or accepted finding keeps its ID and changes
|
|||||||
| F10 | Low | confirmed | Anyone can record acceptance of the licence agreement (`license/lc`) |
|
| F10 | Low | confirmed | Anyone can record acceptance of the licence agreement (`license/lc`) |
|
||||||
| F11 | Low | **fixed in case 4727** | API root page shows licensee, version, licence terms (and DB ID when unlicensed) to anyone |
|
| 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 |
|
| 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 |
|
| F25 | Low | **fixed in case 4727** (upload before rights check accepted) | 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 | **fixed in case 4727** | `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 |
|
| 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 |
|
| 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 |
|
||||||
@@ -554,6 +554,7 @@ translated text is ever rendered as HTML in the client is a pass 9 question. Fix
|
|||||||
by the same ownership check).
|
by the same ownership check).
|
||||||
|
|
||||||
**John, 2026-10-24:** `OverrideUserId` and the export download check in case 4727; the upload written to disk before the rights check is **accepted** (since case 4712 every login that reaches it is staff; a proper fix restructures the upload, a possible later case).
|
**John, 2026-10-24:** `OverrideUserId` and the export download check in case 4727; the upload written to disk before the rights check is **accepted** (since case 4712 every login that reaches it is staff; a proper fix restructures the upload, a possible later case).
|
||||||
|
**Case 4727, step 5 (2026-10-24):** `OverrideUserId` is used only from SuperUser or a user with change rights on User (the v7 migration's need), otherwise ignored (proved: a Service user's attachment claiming SuperUser was recorded as SuperUser's on the old code, as the Service user's now). Export files are named `{type}-u{userId}-{date}.json` and `export/download` serves only a `.json` name carrying the download-key user's own id, so it no longer serves other temporary files either; test `ExportOwnFileOnly` (another user's key downloaded the file on the old code).
|
||||||
|
|
||||||
OK in this pass: attachments are served as downloads (`Content-Disposition: attachment`), stored
|
OK in this pass: attachments are served as downloads (`Content-Disposition: attachment`), stored
|
||||||
under content-hash names in the attachments folder; batch move, batch delete and maintenance
|
under content-hash names in the attachments folder; batch move, batch delete and maintenance
|
||||||
|
|||||||
@@ -269,7 +269,9 @@ namespace AyaNova.Api.Controllers
|
|||||||
{
|
{
|
||||||
AttachToAType = uploadFormData.FormFieldData["AttachToAType"].ToString();
|
AttachToAType = uploadFormData.FormFieldData["AttachToAType"].ToString();
|
||||||
//for v8 migrate purposes
|
//for v8 migrate purposes
|
||||||
if (uploadFormData.FormFieldData.ContainsKey("OverrideUserId"))
|
//case 4727 (F25): only SuperUser or a user who may manage users may set it, otherwise it is ignored
|
||||||
|
if (uploadFormData.FormFieldData.ContainsKey("OverrideUserId") &&
|
||||||
|
(UserIdFromContext.Id(HttpContext.Items) == 1 || Authorized.HasModifyRole(HttpContext.Items, AyaType.User)))
|
||||||
OverrideUserId = long.Parse(uploadFormData.FormFieldData["OverrideUserId"].ToString());
|
OverrideUserId = long.Parse(uploadFormData.FormFieldData["OverrideUserId"].ToString());
|
||||||
|
|
||||||
AttachToObjectId = uploadFormData.FormFieldData["AttachToObjectId"].ToString();
|
AttachToObjectId = uploadFormData.FormFieldData["AttachToObjectId"].ToString();
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ namespace AyaNova.Api.Controllers
|
|||||||
log.LogDebug($"Instantiating biz object handler for {selectedRequest.AType}");
|
log.LogDebug($"Instantiating biz object handler for {selectedRequest.AType}");
|
||||||
var biz = BizObjectFactory.GetBizObject(selectedRequest.AType, ct, UserId, UserRoles, UserTranslationId);
|
var biz = BizObjectFactory.GetBizObject(selectedRequest.AType, ct, UserId, UserRoles, UserTranslationId);
|
||||||
log.LogDebug($"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.AType} items");
|
log.LogDebug($"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.AType} items");
|
||||||
string baseFileName = FileUtil.StringToSafeFileName($"{selectedRequest.AType.ToString().ToLowerInvariant()}-{FileUtil.GetSafeDateFileName()}");
|
//case 4727 (F25): the name carries the user's id, so only that user can download it
|
||||||
|
string baseFileName = FileUtil.StringToSafeFileName($"{selectedRequest.AType.ToString().ToLowerInvariant()}-u{UserId}-{FileUtil.GetSafeDateFileName()}");
|
||||||
string outputSourceFileName = baseFileName + ".json";
|
string outputSourceFileName = baseFileName + ".json";
|
||||||
string outputSourceFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputSourceFileName);
|
string outputSourceFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputSourceFileName);
|
||||||
|
|
||||||
@@ -130,13 +131,15 @@ namespace AyaNova.Api.Controllers
|
|||||||
if (!serverState.IsOpen)
|
if (!serverState.IsOpen)
|
||||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||||
|
|
||||||
if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null)
|
var DownloadUser = await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct);
|
||||||
|
if (DownloadUser == null)
|
||||||
{
|
{
|
||||||
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
|
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
|
||||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FileUtil.TemporaryFileExists(fileName))
|
//case 4727 (F25): only the caller's own export file (see the name in render), no other temporary file
|
||||||
|
if (fileName == null || !fileName.Contains($"-u{DownloadUser.Id}-") || !fileName.EndsWith(".json") || !FileUtil.TemporaryFileExists(fileName))
|
||||||
{
|
{
|
||||||
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection
|
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection
|
||||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||||
|
|||||||
Reference in New Issue
Block a user