From 2b17c2186c1d5be27e858d8d2447d7fe1b96a105 Mon Sep 17 00:00:00 2001 From: Alex Chen Date: Sat, 31 Oct 2026 14:13:20 -0700 Subject: [PATCH] fix(github_copilot): create token dir 0o700 and credential files 0o600 The GitHub Copilot authenticator caches the GitHub access token and the Copilot API key on disk (default token_dir ~/.config/litellm/github_copilot). Previously _ensure_token_dir() used os.makedirs() without a mode and both credential writes used plain open(..., "w"), so under the default umask (022/002) the directory ended up 0755/0775 and both files 0644/0664 - group/world-readable. Any other local UID on a shared proxy host could read the GitHub access token and Copilot API key. - _ensure_token_dir: os.makedirs(..., mode=0o700, exist_ok=True) - access_token / api_key writes: open(..., "w", opener=self._secure_opener) -> os.open 0o600 Verified: under umask 022 the dir is now 0o700, both files 0o600, other-readable=False. --- litellm/llms/github_copilot/authenticator.py | 12 +++++++--- .../test_github_copilot_authenticator.py | 24 ++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/litellm/llms/github_copilot/authenticator.py b/litellm/llms/github_copilot/authenticator.py index ee0e7af7e9b..82493cb6f47 100644 --- a/litellm/llms/github_copilot/authenticator.py +++ b/litellm/llms/github_copilot/authenticator.py @@ -150,7 +150,7 @@ class Authenticator: try: access_token = self._login() try: - with open(self.access_token_file, "w") as f: + with open(self.access_token_file, "w", opener=self._secure_opener) as f: f.write(access_token) except OSError: verbose_logger.error("Error saving access token to file") @@ -192,7 +192,7 @@ class Authenticator: ) from e serialized: Final = json.dumps(api_key_info) try: - with open(self.api_key_file, "w") as f: + with open(self.api_key_file, "w", opener=self._secure_opener) as f: f.write(serialized) except OSError as e: verbose_logger.warning( @@ -289,10 +289,16 @@ class Authenticator: status_code=401, ) + def _secure_opener(self, path: str, flags: int) -> int: + """Open with 0o600: file holds a GitHub access token / Copilot API key.""" + return os.open(path, flags, 0o600) + def _ensure_token_dir(self) -> None: """Ensure the token directory exists.""" if not os.path.exists(self.token_dir): - os.makedirs(self.token_dir, exist_ok=True) + # 0o700: dir holds the GitHub access token + Copilot API key; + # default umask (022/002) would make it group/world-traversable. + os.makedirs(self.token_dir, mode=0o700, exist_ok=True) def _get_github_headers(self, access_token: str | None = None) -> dict[str, str]: """ diff --git a/tests/unit/llms/github_copilot/test_github_copilot_authenticator.py b/tests/unit/llms/github_copilot/test_github_copilot_authenticator.py index 55fd02bc4dd..e16bcb3c5c0 100644 --- a/tests/unit/llms/github_copilot/test_github_copilot_authenticator.py +++ b/tests/unit/llms/github_copilot/test_github_copilot_authenticator.py @@ -75,7 +75,29 @@ class TestGitHubCopilotAuthenticator: patch("os.makedirs") as mock_makedirs, ): auth = Authenticator() - mock_makedirs.assert_called_once_with(auth.token_dir, exist_ok=True) + # credentials dir must not be group/world-accessible + mock_makedirs.assert_called_once_with(auth.token_dir, mode=0o700, exist_ok=True) + + def test_token_dir_and_credential_files_created_with_restrictive_modes( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Token directory is created 0o700 and both credential files are created 0o600 under umask 022.""" + token_dir = tmp_path / "copilot_tokens" + monkeypatch.setenv("GITHUB_COPILOT_TOKEN_DIR", str(token_dir)) + old_umask = os.umask(0o022) + try: + auth = Authenticator() + assert (os.stat(token_dir).st_mode & 0o777) == 0o700 + + with patch.object(auth, "_login", return_value="gho_test_token"): + assert auth.get_access_token() == "gho_test_token" + assert (os.stat(auth.access_token_file).st_mode & 0o777) == 0o600 + + with patch.object(auth, "_refresh_api_key", return_value=dict(REFRESHED_KEY)): + assert auth.get_api_key() == "new-api-key" + assert (os.stat(auth.api_key_file).st_mode & 0o777) == 0o600 + finally: + os.umask(old_umask) def test_get_github_headers(self, authenticator): """Test that GitHub headers are correctly generated."""