diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 2ab880d307..ed04b003d8 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2111,7 +2111,16 @@ Http::post('/v1/account/tokens/magic-url') if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } - $url = htmlentities($url); + + $url = \parse_url($url); + $url['path'] = \htmlentities($url['path'] ?? ''); + $url = (isset($url['scheme']) ? $url['scheme'] . '://' : '') . + (isset($url['user']) ? $url['user'] . (isset($url['pass']) ? ':' . $url['pass'] : '') . '@' : '') . + (isset($url['host']) ? $url['host'] : '') . + (isset($url['port']) ? ':' . $url['port'] : '') . + (isset($url['path']) ? $url['path'] : '') . + (isset($url['query']) ? '?' . $url['query'] : '') . + (isset($url['fragment']) ? '#' . $url['fragment'] : ''); if ($phrase === true) { $phrase = Phrase::generate(); @@ -3573,7 +3582,16 @@ Http::post('/v1/account/recovery') throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); } - $url = htmlentities($url); + $url = \parse_url($url); + $url['path'] = \htmlentities($url['path'] ?? ''); + $url = (isset($url['scheme']) ? $url['scheme'] . '://' : '') . + (isset($url['user']) ? $url['user'] . (isset($url['pass']) ? ':' . $url['pass'] : '') . '@' : '') . + (isset($url['host']) ? $url['host'] : '') . + (isset($url['port']) ? ':' . $url['port'] : '') . + (isset($url['path']) ? $url['path'] : '') . + (isset($url['query']) ? '?' . $url['query'] : '') . + (isset($url['fragment']) ? '#' . $url['fragment'] : ''); + $email = \strtolower($email); $profile = $dbForProject->findOne('users', [ @@ -3889,7 +3907,16 @@ Http::post('/v1/account/verifications/email') throw new Exception(Exception::USER_EMAIL_NOT_FOUND); } - $url = htmlentities($url); + $url = \parse_url($url); + $url['path'] = \htmlentities($url['path'] ?? ''); + $url = (isset($url['scheme']) ? $url['scheme'] . '://' : '') . + (isset($url['user']) ? $url['user'] . (isset($url['pass']) ? ':' . $url['pass'] : '') . '@' : '') . + (isset($url['host']) ? $url['host'] : '') . + (isset($url['port']) ? ':' . $url['port'] : '') . + (isset($url['path']) ? $url['path'] : '') . + (isset($url['query']) ? '?' . $url['query'] : '') . + (isset($url['fragment']) ? '#' . $url['fragment'] : ''); + if ($user->getAttribute('emailVerification')) { throw new Exception(Exception::USER_EMAIL_ALREADY_VERIFIED); } diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index a674cdc40f..28d58ec770 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -498,7 +498,18 @@ Http::post('/v1/teams/:teamId/memberships') $isAppUser = User::isApp($authorization->getRoles()); $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); - $url = htmlentities($url); + if (!empty($url)) { + $url = \parse_url($url); + $url['path'] = \htmlentities($url['path'] ?? ''); + $url = (isset($url['scheme']) ? $url['scheme'] . '://' : '') . + (isset($url['user']) ? $url['user'] . (isset($url['pass']) ? ':' . $url['pass'] : '') . '@' : '') . + (isset($url['host']) ? $url['host'] : '') . + (isset($url['port']) ? ':' . $url['port'] : '') . + (isset($url['path']) ? $url['path'] : '') . + (isset($url['query']) ? '?' . $url['query'] : '') . + (isset($url['fragment']) ? '#' . $url['fragment'] : ''); + + } if (empty($url)) { if (!$isAppUser && !$isPrivilegedUser) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'URL is required'); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 2909ba47f4..14e310ff97 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -6597,4 +6597,105 @@ class ProjectsConsoleClientTest extends Scope } } } + + public function testPasswordRecoveryUrlParams(): void + { + // With search params + $url = 'http://localhost/auth/signin?id=abcd1234&tenant=efgh5678&domain=example.com&referred=0'; + + $response = $this->client->call( + Client::METHOD_POST, + '/account/recovery', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), + [ + 'userId' => ID::unique(), + 'email' => $this->getUser()['email'], + 'url' => $url, + ] + ); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['userId']); + + $userId = $response['body']['userId']; + + $lastEmail = $this->getLastEmail(); + + $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); + $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); + + $expectedUrl = $url . "&userId=" . $userId . "&secret="; + + $this->assertStringContainsString($expectedUrl, $lastEmail['html']); + + // Without search params + $url = 'http://localhost/auth/signin'; + + $response = $this->client->call( + Client::METHOD_POST, + '/account/recovery', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), + [ + 'userId' => ID::unique(), + 'email' => $this->getUser()['email'], + 'url' => $url, + ] + ); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['userId']); + + $userId = $response['body']['userId']; + + $lastEmail = $this->getLastEmail(); + + $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); + $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); + + $expectedUrl = $url . "?userId=" . $userId . "&secret="; + + $this->assertStringContainsString($expectedUrl, $lastEmail['html']); + + // With injection + $url = 'http://localhost/auth/signin\">