diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index f1051497a8..62b4953e27 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -64,8 +64,9 @@ 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 --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 @@ -78,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 diff --git a/app/controllers/general.php b/app/controllers/general.php index c18f729c44..225762ce7e 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -799,7 +799,6 @@ App::init() ->inject('getProjectDB') ->inject('locale') ->inject('localeCodes') - ->inject('clients') ->inject('geodb') ->inject('queueForStatsUsage') ->inject('queueForEvents') @@ -810,7 +809,9 @@ App::init() ->inject('previewHostname') ->inject('devKey') ->inject('apiKey') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, 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, Log $log, 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 */ @@ -942,42 +943,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()) && @@ -1049,7 +1017,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()) { @@ -1070,6 +1038,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()); } diff --git a/app/init/resources.php b/app/init/resources.php index 5f65fe683f..aa04b46e1f 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -944,3 +944,52 @@ 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); + $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)) { + return $referrer; + } + + // Safe if added as web platform + $validator = new Hostname($clients); + if ($validator->isValid($origin)) { + return $referrer; + } + + // 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()) { + return $referrer; + } + } + + // Unsafe; Localhost is always safe for ease of local development + $origin = 'localhost'; + $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']);