CVE-2022-23651 (published 2022-02-23T23:15Z)

b2-sdk-python is a python library to access cloud storage provided by backblaze. Linux and Mac releases of the SDK version 1.14.0 and below contain a key disclosure vulnerability that, in certain conditions, can be exploited by local attackers through a time-of-check-time-of-use (TOCTOU) race condition. SDK users of the SqliteAccountInfo format are vulnerable while users of the InMemoryAccountInfo format are safe. The SqliteAccountInfo saves API keys (and bucket name-to-id mapping) in a local database file ($XDG_CONFIG_HOME/b2/account_info, ~/.b2_account_info or a user-defined path). When first created, the file is world readable and is (typically a few milliseconds) later altered to be private to the user. If the directory containing the file is readable by a local attacker then during the brief period between file creation and permission modification, a local attacker can race to open the file and maintain a handle to it. This allows the local attacker to read the contents after the file after the sensitive information has been saved to it. Consumers of this SDK who rely on it to save data using SqliteAccountInfo class should upgrade to the latest version of the SDK. Those who believe a local user might have opened a handle using this race condition, should remove the affected database files and regenerate all application keys. Users should upgrade to b2-sdk-python 1.14.1 or later.

Severity: LOW
CVSS v2 base score: 1.9 (AV:L/AC:M/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 4.7, MEDIUM (CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-367
cwe_name: Time-of-check Time-of-use (TOCTOU) Race Condition
description: The product checks the state of a resource before using that resource, but the resource's state can change between the check and the use in a way that invalidates the results of the check. This can cause the product to perform invalid actions when the resource is in an unexpected state.
extended_description: This weakness can be security-relevant when an attacker can influence the state of the resource between check and use. This can happen with shared resources such as files, memory, or even variables in multithreaded programs.
         

Fix information:
repo_url: https://github.com/Backblaze/b2-sdk-python
commit_details: repo_url: https://github.com/Backblaze/b2-sdk-python
author: Paweł Polewicz
author_date: 2022-02-23 19:58:01+01:00
msg: Merge pull request from GHSA-p867-fxfr-ph2w

* Fix setting permissions for local sqlite database

Thanks to Jan Schejbal for responsible disclosure!

There used to be a brief moment between creation of the sqlite
database and applying chmod, now there is no such delay.

* Rename self.home to self.test_home in tests

* Actually call the test cleanup

* Add a test for sqlite permissions

* Skip the sqlite chmod test on Windows
repository: repo_url: https://github.com/Backblaze/b2-sdk-python
repo_name: Backblaze/b2-sdk-python
description: Python library to access B2 cloud storage.
repo_language: Python
owner: Backblaze
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed
 * Fix downloading files with unverified checksum
+* Fix setting permissions for local sqlite database (thanks to Jan Schejbal for responsible disclosure!)
 
 ## [1.14.0] - 2021-12-23
 

programming_language: Markdown
filename: sqlite_account_info.py
change_type: ModificationType.MODIFY
diff: @@ -165,9 +165,16 @@ class SqliteAccountInfo(UrlPoolAccountInfo):
 
     def _create_database(self, last_upgrade_to_run):
         """
-        Make sure that the database is created and sets the file permissions.
+        Make sure that the database is created and has appropriate file permissions.
         This should be done before storing any sensitive data in it.
         """
+        # Prepare a file
+        fd = os.open(
+            self.filename,
+            flags=os.O_RDWR | os.O_CREAT,
+            mode=stat.S_IRUSR | stat.S_IWUSR,
+        )
+        os.close(fd)
         # Create the tables in the database
         conn = self._connect()
         try:
@@ -176,9 +183,6 @@ class SqliteAccountInfo(UrlPoolAccountInfo):
         finally:
             conn.close()
 
-        # Set the file permissions
-        os.chmod(self.filename, stat.S_IRUSR | stat.S_IWUSR)
-
     def _create_tables(self, conn, last_upgrade_to_run):
         conn.execute(
             """

programming_language: Python
filename: test_account_info.py
change_type: ModificationType.MODIFY
diff: @@ -14,6 +14,7 @@ import unittest.mock as mock
 import os
 import platform
 import shutil
+import stat
 import tempfile
 
 import pytest
@@ -319,15 +320,29 @@ class TestSqliteAccountInfo(AccountInfoBase):
             os.unlink(self.db_path)
         except OSError:
             pass
-        self.home = tempfile.mkdtemp()
+        self.test_home = tempfile.mkdtemp()
 
         yield
-        for cleanup_method in [lambda: os.unlink(self.db_path), lambda: shutil.rmtree(self.home)]:
+        for cleanup_method in [
+            lambda: os.unlink(self.db_path), lambda: shutil.rmtree(self.test_home)
+        ]:
             try:
-                cleanup_method
+                cleanup_method()
             except OSError:
                 pass
 
+    @pytest.mark.skipif(
+        platform.system() == 'Windows',
+        reason='different permission system on Windows'
+    )
+    def test_permissions(self):
+        """
+        Test that a new database won't be readable by just any user
+        """
+        s = SqliteAccountInfo(file_name=self.db_path,)
+        mode = os.stat(self.db_path).st_mode
+        assert stat.filemode(mode) == '-rw-------'
+
     def test_corrupted(self):
         """
         Test that a corrupted file will be replaced with a blank file.
@@ -371,7 +386,7 @@ class TestSqliteAccountInfo(AccountInfoBase):
         :param dict env: Override Environment variables.
         """
         # Override HOME to ensure hermetic tests
-        with mock.patch('os.environ', env or {'HOME': self.home}):
+        with mock.patch('os.environ', env or {'HOME': self.test_home}):
             return SqliteAccountInfo(
                 file_name=self.db_path if not env else None,
                 last_upgrade_to_run=last_upgrade_to_run,
@@ -380,24 +395,24 @@ class TestSqliteAccountInfo(AccountInfoBase):
     def test_uses_default(self):
         account_info = self._make_sqlite_account_info(
             env={
-                'HOME': self.home,
-                'USERPROFILE': self.home,
+                'HOME': self.test_home,
+                'USERPROFILE': self.test_home,
             }
         )
         actual_path = os.path.abspath(account_info.filename)
-        assert os.path.join(self.home, '.b2_account_info') == actual_path
+        assert os.path.join(self.test_home, '.b2_account_info') == actual_path
 
     def test_uses_xdg_config_home(self, apiver):
         with WindowsSafeTempDir() as d:
             account_info = self._make_sqlite_account_info(
                 env={
-                    'HOME': self.home,
-                    'USERPROFILE': self.home,
+                    'HOME': self.test_home,
+                    'USERPROFILE': self.test_home,
                     XDG_CONFIG_HOME_ENV_VAR: d,
                 }
             )
             if apiver in ['v0', 'v1']:
-                expected_path = os.path.abspath(os.path.join(self.home, '.b2_account_info'))
+                expected_path = os.path.abspath(os.path.join(self.test_home, '.b2_account_info'))
             else:
                 assert os.path.exists(os.path.join(d, 'b2'))
                 expected_path = os.path.abspath(os.path.join(d, 'b2', 'account_info'))
@@ -406,12 +421,12 @@ class TestSqliteAccountInfo(AccountInfoBase):
 
     def test_uses_existing_file_and_ignores_xdg(self):
         with WindowsSafeTempDir() as d:
-            default_db_file_location = os.path.join(self.home, '.b2_account_info')
+            default_db_file_location = os.path.join(self.test_home, '.b2_account_info')
             open(default_db_file_location, 'a').close()
             account_info = self._make_sqlite_account_info(
                 env={
-                    'HOME': self.home,
-                    'USERPROFILE': self.home,
+                    'HOME': self.test_home,
+                    'USERPROFILE': self.test_home,
                     XDG_CONFIG_HOME_ENV_VAR: d,
                 }
             )
@@ -423,8 +438,8 @@ class TestSqliteAccountInfo(AccountInfoBase):
         with WindowsSafeTempDir() as d:
             account_info = self._make_sqlite_account_info(
                 env={
-                    'HOME': self.home,
-                    'USERPROFILE': self.home,
+                    'HOME': self.test_home,
+                    'USERPROFILE': self.test_home,
                     XDG_CONFIG_HOME_ENV_VAR: d,
                     B2_ACCOUNT_INFO_ENV_VAR: os.path.join(d, 'b2_account_info'),
                 }

programming_language: Python