Compare commits

...
Author SHA1 Message Date
Khushboo VermaandGitHub 01305007a2 Merge branch '1.7.x' into feat-SER-67-restrict-invalid-domains 2025-08-04 13:02:31 +05:30
Harsh Mahajan 7f00e497f5 fix:added some code back 2025-08-02 12:58:26 +05:30
Khushboo Verma 6d4e03e6fc Merge branch '1.7.x' into feat-SER-67-restrict-invalid-domains 2025-08-01 17:38:24 +05:30
Khushboo Verma 6f50b4712f Clean up AppwriteDomain validator 2025-08-01 17:36:11 +05:30
Khushboo VermaandGitHub 5ed0721c36 Merge branch '1.7.x' into feat-SER-67-restrict-invalid-domains 2025-08-01 13:22:44 +05:30
Harsh Mahajan 1093251990 added explicit http and https rejection 2025-08-01 12:48:52 +05:30
Harsh Mahajan e780c5fa4a fix: removed redundant filtering 2025-08-01 12:06:35 +05:30
Harsh Mahajan bf0fb14631 fix:remove redundant checks and chnaged port back 2025-08-01 11:17:29 +05:30
Harsh Mahajan 16573dcab7 fix:remove redundant checks 2025-08-01 11:16:29 +05:30
Harsh Mahajan 2809b5f31d fix:redundant check 2025-07-31 23:20:59 +05:30
Harsh Mahajan 0feae3ba27 fix: The code now correctly uses only AppwriteDomain since it extends Domain and includes all the base domain validation logic 2025-07-31 22:44:42 +05:30
Harsh Mahajan a3cf19410c fix:chnaged directory from netwrok to domain 2025-07-31 17:53:04 +05:30
Harsh MahajanandGitHub 47edd6c0c3 Merge branch '1.7.x' into feat-SER-67-restrict-invalid-domains 2025-07-31 16:56:48 +05:30
Harsh Mahajan 79d7b124e5 fix: add rejection for localhost and api.localhost domain 2025-07-30 17:41:22 +05:30
Harsh Mahajan f2a7f7f132 fix: enhance AppwriteDomain validator with URL protocol support 2025-07-30 17:22:25 +05:30
Harsh Mahajan de12173fca feat: added Global forbidden prefixes check (commit-, branch-) 2025-07-30 17:05:06 +05:30
Harsh MahajanandGitHub 1a084d7acc Merge branch '1.7.x' into feat-SER-67-restrict-invalid-domains 2025-07-30 16:47:40 +05:30
Harsh Mahajan f722f5fd35 feat: enhance AppwriteDomain validator with comprehensive validation rules 2025-07-30 16:47:20 +05:30
Harsh MahajanandGitHub 6ef76472e5 Merge branch '1.7.x' into feat-SER-67-restrict-invalid-domains 2025-07-30 12:05:17 +05:30
Harsh Mahajan b6f9980e02 add AppwriteDomain validator with inheritance approach 2025-07-29 13:49:51 +05:30
Harsh Mahajan 80b6347edf ha 2025-07-29 12:19:13 +05:30
Harsh Mahajan 0c5df2da5a fix:added chnages 2025-07-29 12:18:40 +05:30
Harsh MahajanandGitHub 677c2952e8 Merge branch '1.7.x' into feat-SER-67-restrict-invalid-domains 2025-07-29 11:49:35 +05:30
Harsh Mahajan 8be36a3664 fix:lint errors 2025-07-29 00:59:24 +05:30
Harsh Mahajan 2be9b50815 fix:created a constant for appwrite.network 2025-07-29 00:53:36 +05:30
Harsh Mahajan 3441903e7e fix:chnage port to original 2025-07-29 00:28:50 +05:30
Harsh MahajanandGitHub ecf163b197 Merge branch '1.7.x' into feat-SER-67-restrict-invalid-domains 2025-07-29 00:19:45 +05:30
Harsh MahajanandGitHub f48a620bd8 Merge branch 'main' into feat-SER-67-restrict-invalid-domains 2025-07-29 00:17:47 +05:30
Harsh Mahajan a934ba24e8 feat: add AppwriteDomain validator to restrict sub-subdomains and invalid formats 2025-07-29 00:11:31 +05:30
Steven NguyenandGitHub d939415c3a Merge pull request #10200 from appwrite/docs-contributing-how-to-start
docs: update CONTRIBUTING.md to clarify how to start
2025-07-23 18:14:30 -07:00
Steven NguyenandGitHub 73da590d65 docs: update CONTRIBUTING.md to clarify how to start 2025-07-23 12:31:28 -07:00
7 changed files with 239 additions and 27 deletions
@@ -0,0 +1,81 @@
<?php
namespace Appwrite\Domain\Validator;
use Utopia\System\System;
use Utopia\Validator\Domain;
class AppwriteDomain extends Domain
{
public function getDescription(): string
{
return 'Value must be a valid one-level subdomain';
}
public function isValid($value): bool
{
if (empty($value)) {
return false;
}
// Reject domains with leading/trailing whitespace
if (!is_string($value) || $value !== trim($value)) {
return false;
}
if (\filter_var($value, FILTER_VALIDATE_DOMAIN) === false) {
return false;
}
if (preg_match('/^https?:\/\//', $value)) {
return false;
}
if (str_starts_with($value, '.') || str_ends_with($value, '.') || str_contains($value, '..')) {
return false;
}
$domain = strtolower($value);
$parts = explode('.', $domain);
$firstLabel = $parts[0];
if (str_starts_with($firstLabel, 'commit-') || str_starts_with($firstLabel, 'branch-')) {
return false;
}
$managedDomains = [
System::getEnv('_APP_DOMAIN_FUNCTIONS'),
System::getEnv('_APP_DOMAIN_SITES')
];
foreach ($managedDomains as $managedDomain) {
if (empty($managedDomain)) {
continue;
}
// Block exact match
if ($domain === $managedDomain) {
return false;
}
// Validate subdomains - reject sub-subdomains
if (str_ends_with($domain, '.' . $managedDomain)) {
$subdomain = substr($domain, 0, -strlen('.' . $managedDomain));
// Reject sub-subdomains (contains dots) or invalid subdomain format
if (
$subdomain === '' ||
strpos($subdomain, '.') !== false ||
strlen($subdomain) > 63 ||
!preg_match('/^[a-z0-9-]+$/i', $subdomain) ||
str_starts_with($subdomain, '-') ||
str_ends_with($subdomain, '-')
) {
return false;
}
}
}
return true;
}
}
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Console\Http\Resources;
use Appwrite\Domain\Validator\AppwriteDomain;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -13,7 +14,6 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Domain;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -67,10 +67,10 @@ class Get extends Action
Database $dbForPlatform
) {
if ($type === 'rules') {
$validator = new Domain($value);
$appwriteDomainValidator = new AppwriteDomain();
if (!$validator->isValid($value)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $validator->getDescription());
if (!$appwriteDomainValidator->isValid($value)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Value must be a valid domain name or a valid Appwrite subdomain.');
}
$document = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API;
use Appwrite\Domain\Validator\AppwriteDomain;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
@@ -19,7 +20,6 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\IP;
class Create extends Action
@@ -60,7 +60,7 @@ class Create extends Action
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('domain', null, new AppwriteDomain(), 'Domain name.')
->inject('response')
->inject('project')
->inject('queueForCertificates')
@@ -102,10 +102,6 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function;
use Appwrite\Domain\Validator\AppwriteDomain;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
@@ -20,7 +21,6 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\IP;
use Utopia\Validator\Text;
@@ -62,7 +62,7 @@ class Create extends Action
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('domain', null, new AppwriteDomain(), 'Domain name.')
->param('functionId', '', new UID(), 'ID of function to be executed.')
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
->inject('response')
@@ -107,10 +107,6 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect;
use Appwrite\Domain\Validator\AppwriteDomain;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
@@ -20,7 +21,6 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\IP;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
@@ -63,7 +63,7 @@ class Create extends Action
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('domain', null, new AppwriteDomain(), 'Domain name.')
->param('url', null, new URL(), 'Target URL of redirection')
->param('statusCode', null, new WhiteList([301, 302, 307, 308]), 'Status code of redirection')
->param('resourceId', '', new UID(), 'ID of parent resource.')
@@ -110,10 +110,6 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site;
use Appwrite\Domain\Validator\AppwriteDomain;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
@@ -20,7 +21,6 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\IP;
use Utopia\Validator\Text;
@@ -62,7 +62,7 @@ class Create extends Action
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('domain', null, new AppwriteDomain(), 'Domain name.')
->param('siteId', '', new UID(), 'ID of site to be executed.')
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
->inject('response')
@@ -107,9 +107,6 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
try {
$domain = new Domain($domain);
@@ -0,0 +1,146 @@
<?php
namespace Tests\Unit\Domain\Validators;
use Appwrite\Domain\Validator\AppwriteDomain;
use PHPUnit\Framework\TestCase;
class AppwriteDomainTest extends TestCase
{
protected ?AppwriteDomain $validator = null;
public function setUp(): void
{
$this->validator = new AppwriteDomain();
}
public function tearDown(): void
{
$this->validator = null;
}
public function testIsValid(): void
{
$sitesDomain = \Utopia\System\System::getEnv('_APP_DOMAIN_SITES');
$functionsDomain = \Utopia\System\System::getEnv('_APP_DOMAIN_FUNCTIONS');
if (!empty($sitesDomain)) {
$this->assertEquals(true, $this->validator->isValid('api.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('test.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('myapp.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('staging.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('prod.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('app123.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('test-app.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('my-awesome-app.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('a.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('x1.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('api.dev.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('foo.bar.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('app.staging.test.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('sub.domain.example.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('API.' . strtoupper($sitesDomain)));
$this->assertEquals(true, $this->validator->isValid('Test.' . ucfirst($sitesDomain)));
$this->assertEquals(true, $this->validator->isValid('MyApp.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('my app.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('test .' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid(' api.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('api.' . $sitesDomain . ' '));
$this->assertEquals(false, $this->validator->isValid('app@test.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('app#test.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('app$test.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('app%test.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('app_test.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('.api.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('api.' . $sitesDomain . '.'));
$this->assertEquals(false, $this->validator->isValid('api..' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid($sitesDomain));
$this->assertEquals(false, $this->validator->isValid('.' . $sitesDomain . '.'));
$this->assertEquals(false, $this->validator->isValid('..' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('commit-api.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('commit-test.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('commit-123.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('branch-api.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('branch-test.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('branch-123.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('COMMIT-api.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('BRANCH-test.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('commitment.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('branching.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('my-commit.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('my-branch.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('pre-commit.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('post-branch.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('.api.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('api..' . $sitesDomain));
}
if (!empty($functionsDomain)) {
$this->assertEquals(true, $this->validator->isValid('api.' . $functionsDomain));
$this->assertEquals(true, $this->validator->isValid('test.' . $functionsDomain));
$this->assertEquals(true, $this->validator->isValid('myapp.' . $functionsDomain));
$this->assertEquals(false, $this->validator->isValid('api.dev.' . $functionsDomain));
$this->assertEquals(false, $this->validator->isValid('foo.bar.' . $functionsDomain));
}
$this->assertEquals(true, $this->validator->isValid('example.com'));
$this->assertEquals(true, $this->validator->isValid('api.example.com'));
$this->assertEquals(true, $this->validator->isValid('test.google.com'));
$this->assertEquals(true, $this->validator->isValid('app.github.io'));
$this->assertEquals(true, $this->validator->isValid('myapp.herokuapp.com'));
$this->assertEquals(true, $this->validator->isValid('sub.domain.example.com'));
// Invalid subdomain formats
$this->assertEquals(false, $this->validator->isValid('-api.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('api-.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('-test-.' . $sitesDomain));
$this->assertEquals(false, $this->validator->isValid('-.' . $sitesDomain));
// Too long subdomain (over 63 characters)
$longSubdomain = str_repeat('a', 64) . '.' . $sitesDomain;
$this->assertEquals(false, $this->validator->isValid($longSubdomain));
// Exactly 63 characters should be valid
$maxLengthSubdomain = str_repeat('a', 63) . '.' . $sitesDomain;
$this->assertEquals(true, $this->validator->isValid($maxLengthSubdomain));
// Single character subdomain should be valid
$this->assertEquals(true, $this->validator->isValid('a.' . $sitesDomain));
// Numbers in subdomain
$this->assertEquals(true, $this->validator->isValid('123.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('api123.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('123api.' . $sitesDomain));
// Mixed case with hyphens
$this->assertEquals(true, $this->validator->isValid('My-Test-App.' . $sitesDomain));
$this->assertEquals(true, $this->validator->isValid('app-v2.' . $sitesDomain));
}
public function testGetType(): void
{
$this->assertEquals('string', $this->validator->getType());
}
public function testIsArray(): void
{
$this->assertEquals(false, $this->validator->isArray());
}
public function testGetDescription(): void
{
$description = $this->validator->getDescription();
$this->assertIsString($description);
$this->assertNotEmpty($description);
$this->assertStringContainsString('one-level subdomain', $description);
}
}