CVE-2024-37164: Insufficient Information (NVD-CWE-noinfo)
value: Computer Vision Annotation Tool (CVAT) is an interactive video and image annotation tool for computer vision. CVAT allows users to supply custom endpoint URLs for cloud storages based on Amazon S3 and Azure Blob Storage. Starting in version 2.1.0 and prior to version 2.14.3, an attacker with a CVAT account can exploit this feature by specifying URLs whose host part is an intranet IP address or an internal domain name. By doing this, the attacker may be able to probe the network that the CVAT backend runs in for HTTP(S) servers. In addition, if there is a web server on this network that is sufficiently API-compatible with an Amazon S3 or Azure Blob Storage endpoint, and either allows anonymous access, or allows authentication with credentials that are known by the attacker, then the attacker may be able to create a cloud storage linked to this server. They may then be able to list files on the server; extract files from the server, if these files are of a type that CVAT supports reading from cloud storage (media data (such as images/videos/archives), importable annotations or datasets, task/project backups); and/or overwrite files on this server with exported annotations/datasets/backups. The exact capabilities of the attacker will depend on how the internal server is configured. Users should upgrade to CVAT 2.14.3 to receive a patch. In this release, the existing SSRF mitigation measures are applied to requests to cloud providers, with access to intranet IP addresses prohibited by default. Some workarounds are also available. One may use network security solutions such as virtual networks or firewalls to prohibit network access from the CVAT backend to unrelated servers on your internal network and/or require authentication for access to internal servers.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-06-13T15:15Z
Repository: https://github.com/cvat-ai/cvat
Language: YAML
Files changed: changelog.d/20240610_143947_roman_ssrf_s3.md, cvat/apps/engine/cloud_provider.py, supervisord/worker.export.conf, tests/allow_minio.sh, tests/docker-compose.minio.yml
Commit date: 2024-06-13 15:16:54 +0300
Commit message: Merge pull request from GHSA-q684-4jjh-83g6

S3 storages support user-specified endpoint URLs, and Azure storages support
user-specified connection strings (which can contain endpoint URLs), so they
are susceptible to SSRF. Make S3 and Azure requests go through smokescreen
to fix this.

AFAIK, there is no way to configure a custom URL for Google Cloud storages,
so those aren't vulnerable.

Co-authored-by: Nikita Manovich <nikita@cvat.ai>
Diff: diff --git a/changelog.d/20240610_143947_roman_ssrf_s3.md b/changelog.d/20240610_143947_roman_ssrf_s3.md
new file mode 100644
index 000000000..5b50b40ba
--- /dev/null
+++ b/changelog.d/20240610_143947_roman_ssrf_s3.md
@@ -0,0 +1,4 @@
+### Security
+
+- Fixed an SSRF vulnerability with custom cloud storage endpoints
+  (<https://github.com/cvat-ai/cvat/security/advisories/GHSA-q684-4jjh-83g6>)
diff --git a/cvat/apps/engine/cloud_provider.py b/cvat/apps/engine/cloud_provider.py
index d76022614..80f907bd2 100644
--- a/cvat/apps/engine/cloud_provider.py
+++ b/cvat/apps/engine/cloud_provider.py
@@ -16,10 +16,11 @@ from concurrent.futures import ThreadPoolExecutor, wait, FIRST_EXCEPTION
 import boto3
 from azure.core.exceptions import HttpResponseError, ResourceExistsError
 from azure.storage.blob import BlobServiceClient, ContainerClient, PublicAccess
 from azure.storage.blob._list_blobs_helper import BlobPrefix
 from boto3.s3.transfer import TransferConfig
+from botocore.client import Config
 from botocore.exceptions import ClientError
 from botocore.handlers import disable_signing
 from datumaro.util import take_by # can be changed to itertools.batched after migration to python3.12
 from django.conf import settings
 from google.cloud import storage
@@ -30,10 +31,11 @@ from rest_framework.exceptions import (NotFound, PermissionDenied,
                                        ValidationError)
 
 from cvat.apps.engine.log import ServerLogManager
 from cvat.apps.engine.models import CloudProviderChoice, CredentialsTypeChoice
 from cvat.apps.engine.utils import get_cpu_number
+from cvat.utils.http import PROXIES_FOR_UNTRUSTED_URLS
 
 class NamedBytesIO(BytesIO):
     @property
     def filename(self) -> Optional[str]:
         return getattr(self, '_filename', None)
@@ -440,11 +442,13 @@ class AWS_S3(_CloudStorage):
         ):
             if arg_v:
                 kwargs[key] = arg_v
 
         session = boto3.Session(**kwargs)
-        self._s3 = session.resource("s3", endpoint_url=endpoint_url)
+        self._s3 = session.resource("s3", endpoint_url=endpoint_url,
+            config=Config(proxies=PROXIES_FOR_UNTRUSTED_URLS or {}),
+        )
 
         # anonymous access
         if not any([access_key_id, secret_key, session_token]):
             self._s3.meta.client.meta.events.register(
                 "choose-signer.s3.*", disable_signing
@@ -642,15 +646,18 @@ class AzureBlobContainer(_CloudStorage):
         prefix: Optional[str] = None,
     ):
         super().__init__(prefix=prefix)
         self._account_name = account_name
         if connection_string:
-            self._blob_service_client = BlobServiceClient.from_connection_string(connection_string)
+            self._blob_service_client = BlobServiceClient.from_connection_string(
+                connection_string, proxies=PROXIES_FOR_UNTRUSTED_URLS)
         elif sas_token:
-            self._blob_service_client = BlobServiceClient(account_url=self.account_url, credential=sas_token)
+            self._blob_service_client = BlobServiceClient(
+                account_url=self.account_url, credential=sas_token, proxies=PROXIES_FOR_UNTRUSTED_URLS)
         else:
-            self._blob_service_client = BlobServiceClient(account_url=self.account_url)
+            self._blob_service_client = BlobServiceClient(
+                account_url=self.account_url, proxies=PROXIES_FOR_UNTRUSTED_URLS)
         self._client = self._blob_service_client.get_container_client(container)
 
     @property
     def container(self) -> ContainerClient:
         return self._client
diff --git a/supervisord/worker.export.conf b/supervisord/worker.export.conf
index 3662e4f91..4e6c18207 100644
--- a/supervisord/worker.export.conf
+++ b/supervisord/worker.export.conf
@@ -23,5 +23,8 @@ command=%(ENV_HOME)s/wait_for_deps.sh
         --worker-class cvat.rqworker.DefaultWorker
 environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler",CVAT_POSTGRES_APPLICATION_NAME="cvat:worker:export"
 numprocs=%(ENV_NUMPROCS)s
 process_name=%(program_name)s-%(process_num)d
 autorestart=true
+
+[program:smokescreen]
+command=smokescreen --listen-ip=127.0.0.1 %(ENV_SMOKESCREEN_OPTS)s
diff --git a/tests/allow_minio.sh b/tests/allow_minio.sh
new file mode 100644
index 000000000..53bd1d063
--- /dev/null
+++ b/tests/allow_minio.sh
@@ -0,0 +1,3 @@
+# Same as allow_webhooks_receiver.sh, but for minio.
+minio_ip_addr="$(getent hosts minio | head -1 | awk '{ print $1 }')"
+export SMOKESCREEN_OPTS="$SMOKESCREEN_OPTS --allow-address=\"$minio_ip_addr\""
diff --git a/tests/docker-compose.minio.yml b/tests/docker-compose.minio.yml
index ffa675f77..6f82aadd1 100644
--- a/tests/docker-compose.minio.yml
+++ b/tests/docker-compose.minio.yml
@@ -1,6 +1,16 @@
+x-allow-minio: &allow-minio
+  depends_on:
+    - minio
+  volumes:
+    - ./tests/allow_minio.sh:/etc/cvat/init.d/allow_minio.sh:ro
+
 services:
+  cvat_server: *allow-minio
+  cvat_worker_export: *allow-minio
+  cvat_worker_import: *allow-minio
+
   minio:
     image: quay.io/minio/minio:RELEASE.2022-09-17T00-09-45Z
     hostname: minio
     restart: always
     command: server /data --console-address ":9001"

Vulnerable code:         self._s3 = session.resource("s3", endpoint_url=endpoint_url)
            self._blob_service_client = BlobServiceClient.from_connection_string(connection_string)
            self._blob_service_client = BlobServiceClient(account_url=self.account_url, credential=sas_token)
            self._blob_service_client = BlobServiceClient(account_url=self.account_url)
Fixed code: ### Security

- Fixed an SSRF vulnerability with custom cloud storage endpoints
  (<https://github.com/cvat-ai/cvat/security/advisories/GHSA-q684-4jjh-83g6>)
from botocore.client import Config
from cvat.utils.http import PROXIES_FOR_UNTRUSTED_URLS
        self._s3 = session.resource("s3", endpoint_url=endpoint_url,
            config=Config(proxies=PROXIES_FOR_UNTRUSTED_URLS or {}),
        )
            self._blob_service_client = BlobServiceClient.from_connection_string(
                connection_string, proxies=PROXIES_FOR_UNTRUSTED_URLS)
            self._blob_service_client = BlobServiceClient(
                account_url=self.account_url, credential=sas_token, proxies=PROXIES_FOR_UNTRUSTED_URLS)
            self._blob_service_client = BlobServiceClient(
                account_url=self.account_url, proxies=PROXIES_FOR_UNTRUSTED_URLS)

[program:smokescreen]
command=smokescreen --listen-ip=127.0.0.1 %(ENV_SMOKESCREEN_OPTS)s
# Same as allow_webhooks_receiver.sh, but for minio.
minio_ip_addr="$(getent hosts minio | head -1 | awk '{ print $1 }')"
export SMOKESCREEN_OPTS="$SMOKESCREEN_OPTS --allow-address=\"$minio_ip_addr\""
x-allow-minio: &allow-minio
  depends_on:
    - minio
  volumes:
    - ./tests/allow_minio.sh:/etc/cvat/init.d/allow_minio.sh:ro

  cvat_server: *allow-minio
  cvat_worker_export: *allow-minio
  cvat_worker_import: *allow-minio