{"record":{"id":"57ac073a8ffc8dee","repo":"fullstackhero/dotnet-starter-kit","slug":"not-your-pending-file","errorCode":null,"errorMessage":"not your pending file","messagePattern":"not your pending file","errorType":"exception","errorClass":"ForbiddenException","httpStatus":403,"severity":"warning","filePath":"src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs","lineNumber":43,"sourceCode":"    IQuotaService quotas,\n    IOutboxWriter outbox,\n    ICurrentUser currentUser)\n    : ICommandHandler<FinalizeUploadCommand, FileAssetDto>\n{\n    public async ValueTask<FileAssetDto> Handle(FinalizeUploadCommand cmd, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(cmd);\n        var tenantId = currentUser.GetTenant() ?? throw new UnauthorizedException(\"invalid tenant\");\n        var userId = currentUser.GetUserId().ToString();\n\n        var asset = await db.FileAssets\n            .FirstOrDefaultAsync(f => f.Id == cmd.FileAssetId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"file not found\");\n\n        if (!string.Equals(asset.CreatedByUserId, userId, StringComparison.Ordinal))\n        {\n            throw new ForbiddenException(\"not your pending file\");\n        }\n        if (asset.Status != FileAssetStatus.PendingUpload)\n        {\n            throw new CustomException(\"file already finalized\", (IEnumerable<string>?)null, HttpStatusCode.Conflict);\n        }\n\n        var head = await storage.HeadObjectAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false)\n            ?? throw new CustomException(\"upload not received\", (IEnumerable<string>?)null, HttpStatusCode.Conflict);\n\n        // Allow declared+1% slack (S3 may differ slightly on multipart). Reject larger sizes.\n        var maxAllowed = asset.SizeBytes + Math.Max(1024L, asset.SizeBytes / 100);\n        if (head.SizeBytes > maxAllowed)\n        {\n            await storage.RemoveAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);\n            db.FileAssets.Remove(asset);\n            await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n            throw new CustomException(\n                $\"uploaded size ({head.SizeBytes}) exceeds declared ({asset.SizeBytes})\",","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs#L25-L61","documentation":"The pending FileAsset exists but asset.CreatedByUserId does not equal the current user's id (ordinal comparison), so the handler throws ForbiddenException(\"not your pending file\"). Finalization is restricted to the uploader who created the pending record — ownership of the pending upload is captured at init time.","triggerScenarios":"A different authenticated user (or a re-issued token for another identity) calls finalize for someone else's pending upload; service account finalizes on behalf of a user; the same human authenticates with a different user id than during init (tenant admin vs member account).","commonSituations":"Backend job completes the browser upload server-side using a service principal identity; team members sharing presigned URLs then colliding at finalize; user re-login switching identities mid-upload; impersonation sessions changing the effective user.","solutions":["Perform the finalize call with the same authenticated user that initiated the upload.","If a backend must finalize on behalf of users, implement a server-side path that carries the original user id rather than reusing the user endpoint.","Do not share presigned upload URLs plus finalize responsibilities across accounts.","Verify the token's user id matches asset.CreatedByUserId before calling finalize."],"exampleFix":"// before\nawait using (var svc = GetServiceAccountClient()) // service principal ≠ uploader\n    await svc.FinalizeUploadAsync(assetId); // 403\n// after\nawait using (var userClient = GetClientForUser(originalUploaderUserId))\n    await userClient.FinalizeUploadAsync(assetId);","handlingStrategy":"validation","validationCode":"var file = await client.GetFileAsync(assetId);\nif (file.CreatedByUserId != currentUserId)\n    throw new UnauthorizedAccessException(\"Only the uploader who initiated this upload may finalize it.\");","typeGuard":null,"tryCatchPattern":"catch (ForbiddenException e) when (e.Message == \"not your pending file\") {\n    notify(\"This upload belongs to another user.\");\n}","preventionTips":["Keep the whole upload lifecycle (init → PUT → finalize) under one authenticated identity.","Do not finalize on behalf of users with a service principal; add a server-side path if needed.","Avoid sharing presigned URLs across accounts.","Detect identity changes (re-login/impersonation) mid-upload and restart the flow."],"tags":["authorization","forbidden","upload","ownership"],"backgroundTag":"permission-denied","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}