Compare commits

...
Author SHA1 Message Date
Matej Bačo 2ad75cae34 Fix failing tests 2026-03-27 16:46:26 +01:00
Matej Bačo 3bb9f03b34 Simplify & add tests 2026-03-27 16:37:15 +01:00
Matej Bačo 1b793ffd1a Filter insecure patterns in user name 2026-03-27 16:10:24 +01:00
7 changed files with 175 additions and 0 deletions
+3
View File
@@ -15,6 +15,7 @@ use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception;
use Appwrite\Filter\Name;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Redirect;
use Appwrite\OpenSSL\OpenSSL;
@@ -404,6 +405,7 @@ Http::post('/v1/account')
->inject('authorization')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Hooks $hooks) {
$name = (new Name())->apply($name);
$email = \strtolower($email);
if ('console' === $project->getId()) {
@@ -3179,6 +3181,7 @@ Http::patch('/v1/account/name')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $name, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
$name = (new Name())->apply($name);
$user->setAttribute('name', $name);
+5
View File
@@ -14,6 +14,7 @@ use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Filter\Name;
use Appwrite\Hooks\Hooks;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -76,6 +77,9 @@ use Utopia\Validator\WhiteList;
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
{
$name = $name ?? '';
$name = (new Name())->apply($name);
$plaintextPassword = $password;
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
@@ -1321,6 +1325,7 @@ Http::patch('/v1/users/:userId/name')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $userId, string $name, Response $response, Database $dbForProject, Event $queueForEvents) {
$name = (new Name())->apply($name);
$user = $dbForProject->getDocument('users', $userId);
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace Appwrite\Filter;
class Name implements Filter
{
private const MAX_LENGTH = 32;
public function apply(mixed $input): mixed
{
if (!\is_string($input)) {
return $input;
}
// Remove HTML tags
$input = \strip_tags($input);
$words = \explode(' ', $input);
// Remove emails
$words = \array_filter($words, fn (string $word) => !\str_contains($word, '@') || !\str_contains($word, '.'));
// Remove URLs
$words = \array_filter($words, fn (string $word) => !\str_contains($word, '://') && !\str_starts_with(\strtolower($word), 'www.'));
// Remove phone numbers
$words = \array_filter($words, function (string $word) {
$digitCount = 0;
for ($i = 0, $len = \strlen($word); $i < $len; $i++) {
if (\ctype_digit($word[$i])) {
$digitCount++;
}
}
return $digitCount < 7;
});
$input = \implode(' ', $words);
return \mb_substr($input, 0, self::MAX_LENGTH);
}
}
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Teams\Http\Teams;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Filter\Name;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
@@ -70,6 +71,8 @@ class Create extends Action
public function action(string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents)
{
$name = (new Name())->apply($name);
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp($authorization->getRoles());
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Teams\Http\Teams\Name;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Filter\Name;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
@@ -58,6 +59,8 @@ class Update extends Action
public function action(string $teamId, string $name, Response $response, Database $dbForProject, Event $queueForEvents)
{
$name = (new Name())->apply($name);
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@@ -142,6 +142,48 @@ trait AccountBase
];
}
public function testCreateAccountNameFilter(): void
{
$headers = array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
// Name with email gets stripped
$response = $this->client->call(Client::METHOD_POST, '/account', $headers, [
'userId' => ID::unique(),
'email' => uniqid() . 'filter1@localhost.test',
'password' => 'password',
'name' => 'John user@example.com',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals('John', $response['body']['name']);
// Name with phone number gets stripped
$response = $this->client->call(Client::METHOD_POST, '/account', $headers, [
'userId' => ID::unique(),
'email' => uniqid() . 'filter2@localhost.test',
'password' => 'password',
'name' => 'John +1234567890',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals('John', $response['body']['name']);
// Name with URL gets stripped
$response = $this->client->call(Client::METHOD_POST, '/account', $headers, [
'userId' => ID::unique(),
'email' => uniqid() . 'filter3@localhost.test',
'password' => 'password',
'name' => 'John https://example.com',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals('John', $response['body']['name']);
}
public function testEmailOTPSession(): void
{
$isConsoleProject = $this->getProject()['$id'] === 'console';
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace Tests\Unit\Filter;
use Appwrite\Filter\Name;
use PHPUnit\Framework\TestCase;
class NameTest extends TestCase
{
private Name $filter;
protected function setUp(): void
{
$this->filter = new Name();
}
public function testNonStringInput(): void
{
$this->assertSame(123, $this->filter->apply(123));
$this->assertSame(null, $this->filter->apply(null));
$this->assertSame(true, $this->filter->apply(true));
$this->assertSame([], $this->filter->apply([]));
}
public function testPlainName(): void
{
$this->assertSame('John Doe', $this->filter->apply('John Doe'));
}
public function testHtmlTags(): void
{
$this->assertSame('John Doe', $this->filter->apply('<b>John</b> Doe'));
$this->assertSame('Hello', $this->filter->apply('<a href="http://evil.com">Hello</a>'));
}
public function testEmails(): void
{
$this->assertSame('John', $this->filter->apply('John user@example.com'));
$this->assertSame('John Doe', $this->filter->apply('John test@mail.org Doe'));
}
public function testUrls(): void
{
$this->assertSame('John', $this->filter->apply('John http://example.com'));
$this->assertSame('John', $this->filter->apply('John https://example.com'));
$this->assertSame('Visit', $this->filter->apply('Visit www.example.com'));
$this->assertSame('Visit', $this->filter->apply('Visit WWW.EXAMPLE.COM'));
}
public function testPhoneNumbers(): void
{
$this->assertSame('John', $this->filter->apply('John 1234567'));
$this->assertSame('John', $this->filter->apply('John +1-234-567-8901'));
$this->assertSame('Call', $this->filter->apply('Call 555-123-4567'));
}
public function testShortNumbersKept(): void
{
$this->assertSame('Agent 007', $this->filter->apply('Agent 007'));
$this->assertSame('Room 404', $this->filter->apply('Room 404'));
}
public function testMaxLength(): void
{
$long = str_repeat('A', 50);
$this->assertSame(str_repeat('A', 32), $this->filter->apply($long));
}
public function testCombined(): void
{
$this->assertSame('John', $this->filter->apply('<b>John</b> user@example.com https://evil.com +1234567890'));
}
public function testEmptyString(): void
{
$this->assertSame('', $this->filter->apply(''));
}
}