Fix url params in redirect URLs

This commit is contained in:
Matej Bačo
2026-02-20 11:15:59 +01:00
parent b9ea24eeb8
commit e2bc3afce6
3 changed files with 143 additions and 4 deletions
+30 -3
View File
@@ -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);
}
+12 -1
View File
@@ -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');
@@ -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\"></a><h1>INJECTED</h1>';
$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']);
$this->assertStringContainsString('INJECTED', $lastEmail['html']);
$this->assertStringNotContainsString('<h1>', $lastEmail['html']);
$this->assertStringNotContainsString('</h1>', $lastEmail['html']);
$sanitizedUrl = \htmlentities($url);
$expectedUrl = $sanitizedUrl . "?userId=" . $userId . "&secret=";
$this->assertStringContainsString($expectedUrl, $lastEmail['html']);
}
}