From fc330dad291bb48d0dbdb182577edce49a52cd00 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 10 Nov 2025 17:08:43 +0530 Subject: [PATCH] feat: add metric validation in StatsUsage worker Added validation to ensure only metrics defined in constants.php are processed by the StatsUsage worker. This prevents unknown or invalid metrics from being stored in the database. Changes: - Added $validMetrics array with all static metric constants - Added $validMetricPatterns array with regex patterns for dynamic metrics - Added isValidMetric() method to validate metric keys - Updated action() method to skip invalid metrics with console warnings This improves security and data integrity by rejecting unknown metrics. --- src/Appwrite/Platform/Workers/StatsUsage.php | 154 ++++++++++++++++++- 1 file changed, 150 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Workers/StatsUsage.php b/src/Appwrite/Platform/Workers/StatsUsage.php index 018c192647..53ddb3a468 100644 --- a/src/Appwrite/Platform/Workers/StatsUsage.php +++ b/src/Appwrite/Platform/Workers/StatsUsage.php @@ -85,6 +85,124 @@ class StatsUsage extends Action '.databases.storage' ]; + /** + * Valid metrics defined in constants.php + * Metrics with placeholders use regex patterns for validation + */ + protected array $validMetrics = [ + METRIC_TEAMS, + METRIC_USERS, + METRIC_WEBHOOKS_SENT, + METRIC_WEBHOOKS_FAILED, + METRIC_AUTH_METHOD_PHONE, + METRIC_MESSAGES, + METRIC_MESSAGES_SENT, + METRIC_MESSAGES_FAILED, + METRIC_SESSIONS, + METRIC_DATABASES, + METRIC_COLLECTIONS, + METRIC_DATABASES_STORAGE, + METRIC_DOCUMENTS, + METRIC_DATABASES_OPERATIONS_READS, + METRIC_DATABASES_OPERATIONS_WRITES, + METRIC_BUCKETS, + METRIC_FILES, + METRIC_FILES_STORAGE, + METRIC_FILES_TRANSFORMATIONS, + METRIC_FILES_IMAGES_TRANSFORMED, + METRIC_SITES, + METRIC_FUNCTIONS, + METRIC_DEPLOYMENTS, + METRIC_DEPLOYMENTS_STORAGE, + METRIC_BUILDS, + METRIC_BUILDS_SUCCESS, + METRIC_BUILDS_FAILED, + METRIC_BUILDS_STORAGE, + METRIC_BUILDS_COMPUTE, + METRIC_BUILDS_COMPUTE_SUCCESS, + METRIC_BUILDS_COMPUTE_FAILED, + METRIC_BUILDS_MB_SECONDS, + METRIC_EXECUTIONS, + METRIC_EXECUTIONS_COMPUTE, + METRIC_EXECUTIONS_MB_SECONDS, + METRIC_NETWORK_REQUESTS, + METRIC_NETWORK_INBOUND, + METRIC_NETWORK_OUTBOUND, + METRIC_MAU, + METRIC_DAU, + METRIC_WAU, + METRIC_WEBHOOKS, + METRIC_PLATFORMS, + METRIC_PROVIDERS, + METRIC_TOPICS, + METRIC_TARGETS, + METRIC_KEYS, + METRIC_DOMAINS, + METRIC_SITES_REQUESTS, + METRIC_SITES_INBOUND, + METRIC_SITES_OUTBOUND, + METRIC_AVATARS_SCREENSHOTS_GENERATED, + ]; + + /** + * Metrics with dynamic placeholders (need regex validation) + * Pattern => Regex to validate the metric + */ + protected array $validMetricPatterns = [ + METRIC_WEBHOOK_ID_SENT => '/^\d+\.webhooks\.events\.sent$/', + METRIC_WEBHOOK_ID_FAILED => '/^\d+\.webhooks\.events\.failed$/', + METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE => '/^auth\.method\.phone\.[A-Z]{2}$/', + METRIC_MESSAGES_TYPE => '/^messages\.(email|sms|push)$/', + METRIC_MESSAGES_TYPE_SENT => '/^messages\.(email|sms|push)\.sent$/', + METRIC_MESSAGES_TYPE_FAILED => '/^messages\.(email|sms|push)\.failed$/', + METRIC_MESSAGES_TYPE_PROVIDER => '/^messages\.(email|sms|push)\..+$/', + METRIC_MESSAGES_TYPE_PROVIDER_SENT => '/^messages\.(email|sms|push)\..+\.sent$/', + METRIC_MESSAGES_TYPE_PROVIDER_FAILED => '/^messages\.(email|sms|push)\..+\.failed$/', + METRIC_DATABASE_ID_COLLECTIONS => '/^\d+\.collections$/', + METRIC_DATABASE_ID_STORAGE => '/^\d+\.databases\.storage$/', + METRIC_DATABASE_ID_DOCUMENTS => '/^\d+\.documents$/', + METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS => '/^\d+\.\d+\.documents$/', + METRIC_DATABASE_ID_COLLECTION_ID_STORAGE => '/^\d+\.\d+\.databases\.storage$/', + METRIC_DATABASE_ID_OPERATIONS_READS => '/^\d+\.databases\.operations\.reads$/', + METRIC_DATABASE_ID_OPERATIONS_WRITES => '/^\d+\.databases\.operations\.writes$/', + METRIC_BUCKET_ID_FILES_TRANSFORMATIONS => '/^\d+\.files\.transformations$/', + METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED => '/^\d+\.files\.imagesTransformed$/', + METRIC_BUCKET_ID_FILES => '/^\d+\.files$/', + METRIC_BUCKET_ID_FILES_STORAGE => '/^\d+\.files\.storage$/', + METRIC_RESOURCE_TYPE_ID_EXECUTIONS => '/^(functions|sites)\.\d+\.executions$/', + METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE => '/^(functions|sites)\.\d+\.executions\.compute$/', + METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS => '/^(functions|sites)\.\d+\.executions\.mbSeconds$/', + METRIC_RESOURCE_TYPE_ID_BUILDS_SUCCESS => '/^(functions|sites)\.\d+\.builds\.success$/', + METRIC_RESOURCE_TYPE_ID_BUILDS_FAILED => '/^(functions|sites)\.\d+\.builds\.failed$/', + METRIC_RESOURCE_TYPE_ID_BUILDS_COMPUTE => '/^(functions|sites)\.\d+\.builds\.compute$/', + METRIC_RESOURCE_TYPE_ID_BUILDS_COMPUTE_SUCCESS => '/^(functions|sites)\.\d+\.builds\.compute\.success$/', + METRIC_RESOURCE_TYPE_ID_BUILDS_COMPUTE_FAILED => '/^(functions|sites)\.\d+\.builds\.compute\.failed$/', + METRIC_RESOURCE_TYPE_ID_BUILDS_MB_SECONDS => '/^(functions|sites)\.\d+\.builds\.mbSeconds$/', + METRIC_RESOURCE_TYPE_ID_BUILDS => '/^(functions|sites)\.\d+\.builds$/', + METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE => '/^(functions|sites)\.\d+\.builds\.storage$/', + METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS => '/^(functions|sites)\.\d+\.deployments$/', + METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE => '/^(functions|sites)\.\d+\.deployments\.storage$/', + METRIC_RESOURCE_TYPE_EXECUTIONS => '/^(functions|sites)\.executions$/', + METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE => '/^(functions|sites)\.executions\.compute$/', + METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS => '/^(functions|sites)\.executions\.mbSeconds$/', + METRIC_RESOURCE_TYPE_BUILDS_SUCCESS => '/^(functions|sites)\.builds\.success$/', + METRIC_RESOURCE_TYPE_BUILDS_FAILED => '/^(functions|sites)\.builds\.failed$/', + METRIC_RESOURCE_TYPE_BUILDS_COMPUTE => '/^(functions|sites)\.builds\.compute$/', + METRIC_RESOURCE_TYPE_BUILDS_COMPUTE_SUCCESS => '/^(functions|sites)\.builds\.compute\.success$/', + METRIC_RESOURCE_TYPE_BUILDS_COMPUTE_FAILED => '/^(functions|sites)\.builds\.compute\.failed$/', + METRIC_RESOURCE_TYPE_BUILDS_MB_SECONDS => '/^(functions|sites)\.builds\.mbSeconds$/', + METRIC_RESOURCE_TYPE_BUILDS => '/^(functions|sites)\.builds$/', + METRIC_RESOURCE_TYPE_BUILDS_STORAGE => '/^(functions|sites)\.builds\.storage$/', + METRIC_RESOURCE_TYPE_DEPLOYMENTS => '/^(functions|sites)\.deployments$/', + METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE => '/^(functions|sites)\.deployments\.storage$/', + METRIC_SITES_ID_REQUESTS => '/^sites\.\d+\.requests$/', + METRIC_SITES_ID_INBOUND => '/^sites\.\d+\.inbound$/', + METRIC_SITES_ID_OUTBOUND => '/^sites\.\d+\.outbound$/', + METRIC_PROVIDER_TYPE_TARGETS => '/^(email|sms|push)\.targets$/', + METRIC_FUNCTIONS_RUNTIME => '/^functions\.runtimes\..+$/', + METRIC_SITES_FRAMEWORK => '/^sites\.frameworks\..+$/', + ]; + /** * @var callable(): Database */ @@ -124,6 +242,26 @@ class StatsUsage extends Action $this->lastTriggeredTime = time(); } + /** + * Validate if a metric is defined in constants + * @param string $metricKey + * @return bool + */ + protected function isValidMetric(string $metricKey): bool + { + if (in_array($metricKey, $this->validMetrics)) { + return true; + } + + foreach ($this->validMetricPatterns as $pattern => $regex) { + if (preg_match($regex, $metricKey)) { + return true; + } + } + + return false; + } + /** * @param Message $message * @param callable(): Database $getProjectDB @@ -162,13 +300,21 @@ class StatsUsage extends Action $this->stats[$projectId]['project'] = $project; $this->stats[$projectId]['receivedAt'] = DateTime::now(); foreach ($payload['metrics'] ?? [] as $metric) { - $this->keys++; - if (!isset($this->stats[$projectId]['keys'][$metric['key']])) { - $this->stats[$projectId]['keys'][$metric['key']] = $metric['value']; + $metricKey = $metric['key'] ?? ''; + + // Skip invalid metrics + if (!$this->isValidMetric($metricKey)) { + Console::warning('[' . DateTime::now() . '] Skipping unknown metric: ' . $metricKey); continue; } - $this->stats[$projectId]['keys'][$metric['key']] += $metric['value']; + $this->keys++; + if (!isset($this->stats[$projectId]['keys'][$metricKey])) { + $this->stats[$projectId]['keys'][$metricKey] = $metric['value']; + continue; + } + + $this->stats[$projectId]['keys'][$metricKey] += $metric['value']; } // If keys crossed threshold or X time passed since the last send and there are some keys in the array ($this->stats)