From 67c0c8b34ce06179b80ee2e833401388f4d40d92 Mon Sep 17 00:00:00 2001 From: Bala FA Date: Sat, 17 Aug 2024 18:41:24 +0530 Subject: [PATCH 1/4] Error out for invalid object name with '.' and '..' (#1431) Signed-off-by: Bala.FA --- minio/api.py | 36 ++++++++++++++++++------------------ minio/helpers.py | 10 ++++++++++ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/minio/api.py b/minio/api.py index 56af05781..025834044 100644 --- a/minio/api.py +++ b/minio/api.py @@ -65,7 +65,7 @@ from .helpers import (_DEFAULT_USER_AGENT, MAX_MULTIPART_COUNT, MAX_MULTIPART_OBJECT_SIZE, MAX_PART_SIZE, MIN_PART_SIZE, BaseURL, DictType, ObjectWriteResult, ProgressType, - ThreadPool, check_bucket_name, check_non_empty_string, + ThreadPool, check_bucket_name, check_object_name, check_sse, check_ssec, genheaders, get_part_info, headers_to_strings, is_valid_policy_type, makedirs, md5sum_hash, queryencode, read_part_data, sha256_hash) @@ -594,7 +594,7 @@ def select_object_content( print(result.stats()) """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) if not isinstance(request, SelectRequest): raise ValueError("request must be SelectRequest type") body = marshal(request) @@ -1107,7 +1107,7 @@ def fget_object( ) """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) if os.path.isdir(file_path): raise ValueError(f"file {file_path} is a directory") @@ -1227,7 +1227,7 @@ def get_object( response.release_conn() """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) check_ssec(ssec) headers = cast(DictType, ssec.headers() if ssec else {}) @@ -1315,7 +1315,7 @@ def copy_object( print(result.object_name, result.version_id) """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) if not isinstance(source, CopySource): raise ValueError("source must be CopySource type") check_sse(sse) @@ -1535,7 +1535,7 @@ def compose_object( print(result.object_name, result.version_id) """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) if not isinstance(sources, (list, tuple)) or not sources: raise ValueError("sources must be non-empty list or tuple type") i = 0 @@ -1808,7 +1808,7 @@ def put_object( ) """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) check_sse(sse) if tags is not None and not isinstance(tags, Tags): raise ValueError("tags must be Tags type") @@ -2031,7 +2031,7 @@ def stat_object( """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) check_ssec(ssec) headers = cast(DictType, ssec.headers() if ssec else {}) @@ -2089,7 +2089,7 @@ def remove_object( ) """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) self._execute( "DELETE", bucket_name, @@ -2252,7 +2252,7 @@ def get_presigned_url( print(url) """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) if expires.total_seconds() < 1 or expires.total_seconds() > 604800: raise ValueError("expires must be between 1 second to 7 days") @@ -2636,7 +2636,7 @@ def delete_object_tags( client.delete_object_tags("my-bucket", "my-object") """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) query_params = {"versionId": version_id} if version_id else {} query_params["tagging"] = "" self._execute( @@ -2664,7 +2664,7 @@ def get_object_tags( tags = client.get_object_tags("my-bucket", "my-object") """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) query_params = {"versionId": version_id} if version_id else {} query_params["tagging"] = "" try: @@ -2703,7 +2703,7 @@ def set_object_tags( client.set_object_tags("my-bucket", "my-object", tags) """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) if not isinstance(tags, Tags): raise ValueError("tags must be Tags type") body = marshal(Tagging(tags)) @@ -2735,7 +2735,7 @@ def enable_object_legal_hold( client.enable_object_legal_hold("my-bucket", "my-object") """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) body = marshal(LegalHold(True)) query_params = {"versionId": version_id} if version_id else {} query_params["legal-hold"] = "" @@ -2765,7 +2765,7 @@ def disable_object_legal_hold( client.disable_object_legal_hold("my-bucket", "my-object") """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) body = marshal(LegalHold(False)) query_params = {"versionId": version_id} if version_id else {} query_params["legal-hold"] = "" @@ -2798,7 +2798,7 @@ def is_object_legal_hold_enabled( print("legal hold is not enabled on my-object") """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) query_params = {"versionId": version_id} if version_id else {} query_params["legal-hold"] = "" try: @@ -2889,7 +2889,7 @@ def get_object_retention( config = client.get_object_retention("my-bucket", "my-object") """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) query_params = {"versionId": version_id} if version_id else {} query_params["retention"] = "" try: @@ -2927,7 +2927,7 @@ def set_object_retention( client.set_object_retention("my-bucket", "my-object", config) """ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host) - check_non_empty_string(object_name) + check_object_name(object_name) if not isinstance(config, Retention): raise ValueError("config must be Retention type") body = marshal(config) diff --git a/minio/helpers.py b/minio/helpers.py index c7f6c9010..2fb11ea04 100644 --- a/minio/helpers.py +++ b/minio/helpers.py @@ -267,6 +267,16 @@ def check_non_empty_string(string: str | bytes): raise TypeError() from exc +def check_object_name(object_name: str): + """Check whether given object name is valid.""" + check_non_empty_string(object_name) + tokens = object_name.split("/") + if "." in tokens or ".." in tokens: + raise ValueError( + "object name with '.' or '..' path segment is not supported", + ) + + def is_valid_policy_type(policy: str | bytes): """ Validate if policy is type str From ba45d0a0673e7454c7ebf35a1eee8fe92dedf0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9B=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=B2?= <75843336+slistov@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:13:59 +0300 Subject: [PATCH 2/4] Add get_data_usage_info admin API (#1423) --- minio/minioadmin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/minio/minioadmin.py b/minio/minioadmin.py index 75bc7f561..8da638a79 100644 --- a/minio/minioadmin.py +++ b/minio/minioadmin.py @@ -708,6 +708,14 @@ def bucket_quota_get(self, bucket: str) -> str: ) return response.data.decode() + def get_data_usage_info(self): + """Get data usage info""" + response = self._url_open( + "GET", + _COMMAND.DATA_USAGE_INFO, + ) + return response.data.decode() + def get_service_account(self, access_key: str) -> str: """Get information about service account""" response = self._url_open( From 935500acd6a5fc2d9ce02e6fc7ce7bcc45a97c60 Mon Sep 17 00:00:00 2001 From: Minio Trusted Date: Sat, 17 Aug 2024 13:17:25 +0000 Subject: [PATCH 3/4] Update version to next release --- minio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minio/__init__.py b/minio/__init__.py index 303945a61..a59ad98b2 100644 --- a/minio/__init__.py +++ b/minio/__init__.py @@ -33,7 +33,7 @@ __title__ = "minio-py" __author__ = "MinIO, Inc." -__version__ = "7.2.8" +__version__ = "7.2.9" __license__ = "Apache 2.0" __copyright__ = "Copyright 2015, 2016, 2017, 2018, 2019, 2020 MinIO, Inc." From 81a152d75413aadd18318e8217336fe58dd8f184 Mon Sep 17 00:00:00 2001 From: "Bala.FA" Date: Sat, 17 Aug 2024 06:48:57 +0530 Subject: [PATCH 4/4] SignV4: trim leading/trailing spaces in header value Fixes #1434 Signed-off-by: Bala.FA --- minio/signer.py | 2 +- tests/functional/tests.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/minio/signer.py b/minio/signer.py index 3e60beb91..83cf89831 100644 --- a/minio/signer.py +++ b/minio/signer.py @@ -74,7 +74,7 @@ def _get_canonical_headers( ): values = values if isinstance(values, (list, tuple)) else [values] ordered_headers[key] = ",".join([ - _MULTI_SPACE_REGEX.sub(" ", value) for value in values + _MULTI_SPACE_REGEX.sub(" ", value).strip() for value in values ]) ordered_headers = OrderedDict(sorted(ordered_headers.items())) diff --git a/tests/functional/tests.py b/tests/functional/tests.py index 7def5ba85..f6a043f2d 100644 --- a/tests/functional/tests.py +++ b/tests/functional/tests.py @@ -692,7 +692,12 @@ def test_put_object(log_entry, sse=None): reader = LimitedRandomReader(length) log_entry["args"]["data"] = "LimitedRandomReader(11 * MB)" log_entry["args"]["metadata"] = metadata = { - 'x-amz-meta-testing': 'value', 'test-key': 'value2'} + 'x-amz-meta-testing': 'value', + 'test-key': 'value2', + "My-Project": "Project One", + "My-header1": " a b c ", + "My-Header2": "\"a b c\"", + } log_entry["args"]["content_type"] = content_type = ( "application/octet-stream") log_entry["args"]["object_name"] = object_name + "-metadata"