Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog/unreleased/41574
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Bugfix: Fix subadmin email change updating caller's address instead of target's

The verification token and confirmation link in the subadmin path of
setMailAddress were associated with the caller's account instead of the
target user's account. Clicking the confirmation link changed the
subadmin's email rather than the intended target's email.

https://github.com/owncloud/core/pull/41574
2 changes: 1 addition & 1 deletion settings/Controller/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,7 @@ public function setMailAddress($id, $mailAddress) {
}

try {
if ($this->sendEmail($userId, $mailAddress)) {
if ($this->sendEmail($id, $mailAddress)) {
return new DataResponse(
[
'status' => 'success',
Expand Down
75 changes: 75 additions & 0 deletions tests/Settings/Controller/UsersControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2276,6 +2276,81 @@ public function testSetEmailAddress($mailAddress, $isValid, $expectsUpdate, $can
$this->assertSame($responseCode, $response->getStatus());
}

/**
* When a subadmin calls setMailAddress for a different user, the verification
* email and token must be stored under the target user's ID, not the caller's.
* Regression test for GHSA-5945-5fp8-4283 (subadmin path).
*/
public function testSetMailAddressSubadminSendsEmailToTargetNotCaller(): void {
$this->container['IsAdmin'] = false;

$callerUser = $this->createMock(User::class);
$callerUser->method('getUID')->willReturn('subadmin');

$targetUser = $this->createMock(User::class);
$targetUser->method('getUID')->willReturn('targetuser');
$targetUser->method('canChangeMailAddress')->willReturn(true);

$subAdmin = $this->createMock(SubAdmin::class);
$subAdmin->method('isUserAccessible')->willReturn(true);

$this->container['GroupManager']
->method('getSubAdmin')
->willReturn($subAdmin);

$this->container['UserSession']
->method('getUser')
->willReturn($callerUser);

$this->container['UserManager']
->method('get')
->willReturnCallback(function ($id) use ($callerUser, $targetUser) {
$userMap = ['subadmin' => $callerUser, 'targetuser' => $targetUser];
return $userMap[$id] ?? null;
});

$this->container['Mailer']
->method('validateMailAddress')
->willReturn(true);

// Token must be read from and stored under 'targetuser', not 'subadmin'
$this->container['Config']
->method('getUserValue')
->with('targetuser', 'owncloud', 'changeMail')
->willReturn('');

$this->container['Config']
->expects($this->once())
->method('setUserValue')
->with('targetuser', 'owncloud', 'changeMail', $this->anything());

$this->container['TimeFactory']
->method('getTime')
->willReturn(12345);

$this->container['SecureRandom']
->method('generate')
->willReturn('SomeToken');

// Confirmation link must carry 'targetuser', not 'subadmin'
$this->container['URLGenerator']
->expects($this->once())
->method('linkToRouteAbsolute')
->with('settings.Users.changeMail', $this->callback(function ($params) {
return isset($params['userId']) && $params['userId'] === 'targetuser';
}))
->willReturn('https://example.com/changeMail');

$message = $this->createMock(Message::class);
$this->container['Mailer']
->method('createMessage')
->willReturn($message);

$response = $this->container['UsersController']->setMailAddress('targetuser', 'new@example.com');
$this->assertSame(Http::STATUS_OK, $response->getStatus());
$this->assertSame('success', $response->getData()['status']);
}

public function testStatsAdmin(): void {
$this->container['IsAdmin'] = true;

Expand Down
Loading