CVE-2023-32303 (published 2023-05-12T21:15Z)

Planet is software that provides satellite data. The secret file stores the user's Planet API authentication information. It should only be accessible by the user, but before version 2.0.1, its permissions allowed the user's group and non-group to read the file as well. This issue was patched in version 2.0.1. As a workaround, set the secret file permissions to only user read/write by hand.\n

CVSS v3 base score: 5.5, MEDIUM (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-732
cwe_name: Incorrect Permission Assignment for Critical Resource
description: The product specifies permissions for a security-critical resource in a way that allows that resource to be read or modified by unintended actors.
extended_description: When a resource is given a permission setting that provides access to a wider range of actors than required, it could lead to the exposure of sensitive information, or the modification of that resource by unintended parties. This is especially dangerous when the resource is related to program configuration, execution, or sensitive user data. For example, consider a misconfigured storage account for the cloud that can be read or written by a public or anonymous user.
         

Fix information:
repo_url: https://github.com/planetlabs/planet-client-python
commit_details: repo_url: https://github.com/planetlabs/planet-client-python
author: Jennifer Reiber Kyle
author_date: 2023-05-10 13:14:06-07:00
msg: enforce restricting secret file permissions to user read/write
repository: repo_url: https://github.com/planetlabs/planet-client-python
repo_name: planetlabs/planet-client-python
description: Python client for Planet APIs
repo_language: Python
owner: planetlabs
file_changes: filename: auth.py
change_type: ModificationType.MODIFY
diff: @@ -19,7 +19,9 @@ import json
 import logging
 import os
 import pathlib
+import stat
 import typing
+from typing import Optional
 
 import httpx
 import jwt
@@ -27,7 +29,6 @@ import jwt
 from . import http
 from .constants import ENV_API_KEY, PLANET_BASE_URL, SECRET_FILE_PATH
 from .exceptions import AuthException
-from typing import Optional
 
 LOGGER = logging.getLogger(__name__)
 
@@ -226,8 +227,15 @@ class APIKeyAuth(httpx.BasicAuth, Auth):
 
 class _SecretFile:
 
-    def __init__(self, path):
-        self.path = path
+    def __init__(self, path: typing.Union[str, pathlib.Path]):
+        self.path = pathlib.Path(path)
+
+        self.permissions = stat.S_IRUSR | stat.S_IWUSR  # user rw
+
+        # in sdk versions <=2.0.0, secret file was created with the wrong
+        # permissions, fix this automatically as well as catching the unlikely
+        # cases where the permissions get changed externally
+        self._enforce_permissions()
 
     def write(self, contents: dict):
         try:
@@ -240,7 +248,11 @@ class _SecretFile:
 
     def _write(self, contents: dict):
         LOGGER.debug(f'Writing to {self.path}')
-        with open(self.path, 'w') as fp:
+
+        def opener(path, flags):
+            return os.open(path, flags, self.permissions)
+
+        with open(self.path, 'w', opener=opener) as fp:
             fp.write(json.dumps(contents))
 
     def read(self) -> dict:
@@ -248,3 +260,17 @@ class _SecretFile:
         with open(self.path, 'r') as fp:
             contents = json.loads(fp.read())
         return contents
+
+    def _enforce_permissions(self):
+        '''if the file's permissions are not what they should be, fix them'''
+        try:
+            # in octal, permissions is the last three bits of the mode
+            file_permissions = self.path.stat().st_mode & 0o777
+            if file_permissions != self.permissions:
+                LOGGER.debug(
+                    f'{self.path} permissions are {oct(file_permissions)}, '
+                    f'should be {oct(self.permissions)}. Fixing.')
+                self.path.chmod(self.permissions)
+        except FileNotFoundError:
+            # just skip it if the secret file doesn't exist
+            pass

programming_language: Python
filename: test_auth.py
change_type: ModificationType.MODIFY
diff: @@ -31,7 +31,7 @@ def test_secretfile_read():
 
 @pytest.fixture
 def secret_path(monkeypatch, tmp_path):
-    secret_path = str(tmp_path / '.test')
+    secret_path = tmp_path / '.test'
     monkeypatch.setattr(auth, 'SECRET_FILE_PATH', secret_path)
     yield secret_path
 
@@ -138,3 +138,19 @@ def test_Auth_store_exists(tmp_path):
 
     with open(secret_path, 'r') as fp:
         assert json.loads(fp.read()) == {"key": "test", "existing": "exists"}
+
+
+def test__SecretFile_permissions_doesnotexist(secret_path):
+    '''No exception is raised if the file doesn't exist'''
+    auth._SecretFile(secret_path)
+
+
+def test__SecretFile_permissions_incorrect(secret_path):
+    '''Incorrect permissions are fixed'''
+    with open(secret_path, 'w') as fp:
+        fp.write('{"existing": "exists"}')
+
+    secret_path.chmod(0o666)
+
+    auth._SecretFile(secret_path)
+    assert secret_path.stat().st_mode & 0o777 == 0o600

programming_language: Python