fix(api): block cross-user delete and address review feedback (#824)
Review of PR #825 surfaced an auth bypass introduced by adding loginName support to delete_app_password: with the OCS-resolved UID discarded, a user could authenticate as their own loginName (via the request body) while targeting another user's path and delete the victim's stored app password. Add the same UID-mismatch guard provisioning already has, so the authenticated account must own the path UID (403 otherwise). Also: - integration test: build the BasicAuth header via base64 instead of httpx.BasicAuth._auth_header (private attribute); mark the throwaway test credential NOSONAR(S2068). - unit tests: cover the httpx.RequestError -> 502 branch, the standard OCS v2 success shape (meta.statuscode 200), and the cross-user delete 403 guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a11a4e709c
commit
3c55d03ecf
@@ -495,7 +495,7 @@ async def delete_app_password(request: Request) -> JSONResponse:
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
_, error_response = await _validate_nextcloud_credentials(
|
||||
ocs_user_id, error_response = await _validate_nextcloud_credentials(
|
||||
nextcloud_host, login_name, password
|
||||
)
|
||||
if error_response is not None:
|
||||
@@ -507,6 +507,19 @@ async def delete_app_password(request: Request) -> JSONResponse:
|
||||
)
|
||||
return error_response
|
||||
|
||||
# The authenticated account must be the UID whose password is being deleted.
|
||||
# ``_extract_basic_auth`` only checks the BasicAuth *name* field equals the
|
||||
# path UID, not that the supplied credential authenticates as that account —
|
||||
# without this guard a user could authenticate with their own loginName (via
|
||||
# the body) while targeting another user's path and delete the victim's
|
||||
# stored password.
|
||||
if ocs_user_id != path_user_id:
|
||||
logger.warning("User ID mismatch in OCS response for delete")
|
||||
return JSONResponse(
|
||||
{"success": False, "error": "User ID mismatch"},
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
try:
|
||||
storage = await _get_app_password_storage(request)
|
||||
deleted = await storage.delete_app_password(username)
|
||||
|
||||
Reference in New Issue
Block a user