[Critical][Security] IDOR on GET /progress/domain/{domainKey} - any authenticated user can read another user's activity #54

Closed
opened 2026-05-05 04:14:44 +00:00 by spikerj · 1 comment
Owner

Severity: Critical (Security)

File: SpikerSoft.Api/Domain/ActivityTracking/ActivityController.cs (~90-100) + GetDomainDetailQueryHandler.cs

Problem: The endpoint accepts userId as a query parameter and loads UserActivities for that id with no parent/self check. Any authenticated caller can substitute another user's id and read their per-domain activity, including RecentActivityDto.Metadata.

Unlike GetChildKnowledgeProgressQuery which enforces parent/child linkage, this handler has no authorization gate.

Fix: Mirror the child-progress authorization model:

  • If userId == caller.UserId allow it
  • Otherwise require linked-parent relationship or a staff role
  • Reject with 403 otherwise

Acceptance criteria:

  • Self-access works
  • Parent-of-child access works for legitimately linked accounts
  • Cross-user access by unrelated users returns 403
  • Unit test covers all three branches
**Severity:** Critical (Security) **File:** `SpikerSoft.Api/Domain/ActivityTracking/ActivityController.cs` (~90-100) + `GetDomainDetailQueryHandler.cs` **Problem:** The endpoint accepts `userId` as a query parameter and loads `UserActivities` for that id with no parent/self check. Any authenticated caller can substitute another user's id and read their per-domain activity, including `RecentActivityDto.Metadata`. Unlike `GetChildKnowledgeProgressQuery` which enforces parent/child linkage, this handler has no authorization gate. **Fix:** Mirror the child-progress authorization model: - If `userId == caller.UserId` allow it - Otherwise require linked-parent relationship or a staff role - Reject with 403 otherwise **Acceptance criteria:** - [ ] Self-access works - [ ] Parent-of-child access works for legitimately linked accounts - [ ] Cross-user access by unrelated users returns 403 - [ ] Unit test covers all three branches
Author
Owner

Resolved.

Added CallerUserId and BypassAuthorization to GetDomainDetailQuery. The handler now:

  • Allows the call when request.UserId == request.CallerUserId (self)
  • Otherwise loads the target's UserProfile and verifies ParentalControls.ParentKeycloakUserId == CallerUserId
  • Throws UnauthorizedAccessException (mapped to 403 by the controller) when neither check passes
  • Skips the gate entirely when BypassAuthorization == true, set by the controller for staff/admin callers via the new IsStaff() helper

ActivityController.GetDomainDetail now passes GetUserId() and IsStaff() and catches UnauthorizedAccessException -> Forbid().

Files changed:

  • SpikerSoft.Business/Domain/Activity/Queries/GetDomainDetail/GetDomainDetailQuery.cs
  • SpikerSoft.Business/Domain/Activity/Queries/GetDomainDetail/GetDomainDetailQueryHandler.cs
  • SpikerSoft.Api/Domain/ActivityTracking/ActivityController.cs
**Resolved.** Added `CallerUserId` and `BypassAuthorization` to `GetDomainDetailQuery`. The handler now: - Allows the call when `request.UserId == request.CallerUserId` (self) - Otherwise loads the target's `UserProfile` and verifies `ParentalControls.ParentKeycloakUserId == CallerUserId` - Throws `UnauthorizedAccessException` (mapped to 403 by the controller) when neither check passes - Skips the gate entirely when `BypassAuthorization == true`, set by the controller for staff/admin callers via the new `IsStaff()` helper `ActivityController.GetDomainDetail` now passes `GetUserId()` and `IsStaff()` and catches `UnauthorizedAccessException` -> `Forbid()`. Files changed: - `SpikerSoft.Business/Domain/Activity/Queries/GetDomainDetail/GetDomainDetailQuery.cs` - `SpikerSoft.Business/Domain/Activity/Queries/GetDomainDetail/GetDomainDetailQueryHandler.cs` - `SpikerSoft.Api/Domain/ActivityTracking/ActivityController.cs`
Sign in to join this conversation.