From c1bd493fbea286836675458c9f797a886a84f834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 23 Jun 2025 11:28:18 +0200 Subject: [PATCH 1/5] Move refferer to resource; re-use in OPTIONS; re-use in platform check --- app/controllers/general.php | 50 +++++++----------------------------- app/init/resources.php | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 61a9cc9bfa..a47ae510fa 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -793,7 +793,6 @@ App::init() ->inject('getProjectDB') ->inject('locale') ->inject('localeCodes') - ->inject('clients') ->inject('geodb') ->inject('queueForStatsUsage') ->inject('queueForEvents') @@ -804,7 +803,9 @@ App::init() ->inject('previewHostname') ->inject('devKey') ->inject('apiKey') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey) { + ->inject('httpReferrer') + ->inject('httpReferrerSafe') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, string $httpReferrer, string $httpReferrerSafe) { /* * Appwrite Router */ @@ -936,42 +937,9 @@ App::init() $locale->setDefault($localeParam); } - $referrer = $request->getReferer(); - $origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST); - $protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME); - $port = \parse_url($request->getOrigin($referrer), PHP_URL_PORT); - - $refDomainOrigin = 'localhost'; - $validator = new Hostname($clients); - if ($validator->isValid($origin)) { - $refDomainOrigin = $origin; - } elseif (!empty($origin)) { - // Auto-allow domains with linked rule - if (System::getEnv('_APP_RULES_FORMAT') === 'md5') { - $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? ''))); - } else { - $rule = Authorization::skip( - fn () => $dbForPlatform->find('rules', [ - Query::equal('domain', [$origin]), - Query::limit(1) - ]) - )[0] ?? new Document(); - } - - if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) { - $refDomainOrigin = $origin; - } - } - - $refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : ''); - - $refDomain = (!$route->getLabel('origin', false)) // This route is publicly accessible - ? $refDomain - : (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : ''); - + $origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST); $selfDomain = new Domain($request->getHostname()); $endDomain = new Domain((string)$origin); - Config::setParam( 'domainVerification', ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) && @@ -1043,7 +1011,7 @@ App::init() ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Forwarded-For, X-Forwarded-User-Agent') ->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies') - ->addHeader('Access-Control-Allow-Origin', $refDomain) + ->addHeader('Access-Control-Allow-Origin', $httpReferrerSafe) ->addHeader('Access-Control-Allow-Credentials', 'true'); if (!$devKey->isEmpty()) { @@ -1064,6 +1032,7 @@ App::init() && \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE]) && $route->getLabel('origin', false) !== '*' && empty($request->getHeader('x-appwrite-key', '')) + && \parse_url($httpReferrerSafe, PHP_URL_HOST) === 'localhost' ) { throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription()); } @@ -1086,7 +1055,8 @@ App::options() ->inject('project') ->inject('devKey') ->inject('apiKey') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey) { + ->inject('httpReferrerSafe') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, string $httpReferrerSafe) { /* * Appwrite Router */ @@ -1099,14 +1069,12 @@ App::options() } } - $origin = $request->getOrigin(); - $response ->addHeader('Server', 'Appwrite') ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent') ->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies') - ->addHeader('Access-Control-Allow-Origin', $origin) + ->addHeader('Access-Control-Allow-Origin', $httpReferrerSafe) ->addHeader('Access-Control-Allow-Credentials', 'true') ->noContent(); diff --git a/app/init/resources.php b/app/init/resources.php index 5f65fe683f..eb77598adc 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -944,3 +944,54 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) { } return new Document([]); }, ['project', 'dbForProject', 'request']); + +App::setResource('httpReferrer', function (Request $request): string { + $referrer = $request->getReferer(); + return $referrer; +}, ['request']); + +App::setResource('httpReferrerSafe', function (Request $request, string $httpReferrer, array $clients, Database $dbForPlatform, Document $project, App $utopia): string { + $origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST); + + // Safe if route is public + $route = $utopia->getRoute(); + if (!$route->getLabel('origin', false)) { + goto originToUrl; + } + + // Safe if added as web platform + $validator = new Hostname($clients); + if ($validator->isValid($origin)) { + goto originToUrl; + } + + // Safe if rule with same project ID exists + if (!empty($origin)) { + if (System::getEnv('_APP_RULES_FORMAT') === 'md5') { + $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? ''))); + } else { + $rule = Authorization::skip( + fn () => $dbForPlatform->find('rules', [ + Query::equal('domain', [$origin]), + Query::limit(1) + ]) + )[0] ?? new Document(); + } + + if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) { + goto originToUrl; + } + } + + // Unsafe; Localhost is always safe for ease of local development + $origin = 'localhost'; + + originToUrl: + + $protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME); + $port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT); + + $referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : ''); + + return $referrer; +}, ['request', 'httpReferrer', 'clients', 'dbForPlatform', 'project', 'utopia']); From b69ff36ddb2dc59d4699fd0a86d188c2513faaf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 23 Jun 2025 11:57:06 +0200 Subject: [PATCH 2/5] Manual QA fixes --- app/controllers/general.php | 7 ++++--- app/init/resources.php | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index a47ae510fa..3745a41c9a 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1055,8 +1055,7 @@ App::options() ->inject('project') ->inject('devKey') ->inject('apiKey') - ->inject('httpReferrerSafe') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, string $httpReferrerSafe) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey) { /* * Appwrite Router */ @@ -1069,12 +1068,14 @@ App::options() } } + $origin = $request->getOrigin(); + $response ->addHeader('Server', 'Appwrite') ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent') ->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies') - ->addHeader('Access-Control-Allow-Origin', $httpReferrerSafe) + ->addHeader('Access-Control-Allow-Origin', $origin) ->addHeader('Access-Control-Allow-Credentials', 'true') ->noContent(); diff --git a/app/init/resources.php b/app/init/resources.php index eb77598adc..18203869df 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -953,9 +953,9 @@ App::setResource('httpReferrer', function (Request $request): string { App::setResource('httpReferrerSafe', function (Request $request, string $httpReferrer, array $clients, Database $dbForPlatform, Document $project, App $utopia): string { $origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST); - // Safe if route is public + // Safe if route is publicly accessible $route = $utopia->getRoute(); - if (!$route->getLabel('origin', false)) { + if ($route->getLabel('origin', false)) { goto originToUrl; } From df82340daebd23bdb5c0c1931cf3a7793154bdc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 25 Jun 2025 14:22:55 +0200 Subject: [PATCH 3/5] Improve code quality --- app/init/resources.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index 18203869df..aa04b46e1f 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -952,17 +952,20 @@ App::setResource('httpReferrer', function (Request $request): string { App::setResource('httpReferrerSafe', function (Request $request, string $httpReferrer, array $clients, Database $dbForPlatform, Document $project, App $utopia): string { $origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST); + $protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME); + $port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT); + $referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : ''); // Safe if route is publicly accessible $route = $utopia->getRoute(); if ($route->getLabel('origin', false)) { - goto originToUrl; + return $referrer; } // Safe if added as web platform $validator = new Hostname($clients); if ($validator->isValid($origin)) { - goto originToUrl; + return $referrer; } // Safe if rule with same project ID exists @@ -979,19 +982,14 @@ App::setResource('httpReferrerSafe', function (Request $request, string $httpRef } if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) { - goto originToUrl; + return $referrer; } } // Unsafe; Localhost is always safe for ease of local development $origin = 'localhost'; - - originToUrl: - $protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME); $port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT); - $referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : ''); - return $referrer; }, ['request', 'httpReferrer', 'clients', 'dbForPlatform', 'project', 'utopia']); From f1590652cbe63085bfc2449898cefd242d6dbc8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 25 Jun 2025 14:30:50 +0200 Subject: [PATCH 4/5] Improve benchmark logging --- .github/workflows/benchmark.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 6d73787d00..f9e86ce332 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -64,6 +64,7 @@ jobs: sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg sudo apt update sudo apt install oha + oha --version - name: Benchmark PR run: 'oha -z 180s http://localhost/v1/health/version -j > benchmark.json' - name: Cleaning From 4fff9b94f86daa378cd21317c90b52a45a862e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 25 Jun 2025 14:45:58 +0200 Subject: [PATCH 5/5] Fix benchmark param name --- .github/workflows/benchmark.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 2758eed90e..62b4953e27 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -66,7 +66,7 @@ jobs: sudo apt install oha oha --version - name: Benchmark PR - run: 'oha -z 180s http://localhost/v1/health/version --output_format json > benchmark.json' + run: 'oha -z 180s http://localhost/v1/health/version --output-format json > benchmark.json' - name: Cleaning run: docker compose down -v - name: Installing latest version @@ -79,7 +79,7 @@ jobs: docker compose up -d sleep 10 - name: Benchmark Latest - run: oha -z 180s http://localhost/v1/health/version --output_format json > benchmark-latest.json + run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json - name: Prepare comment run: | echo '## :sparkles: Benchmark results' > benchmark.txt