From 57aa8113f1c6bdaad1ceb32f58347d57902c3eac Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:08:44 +0000 Subject: [PATCH] fix(claude-code): allow dot-prefixed segments in git-subdir paths --- .../claude_code_marketplace.py | 10 +++--- .../test_claude_code_marketplace.py | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py b/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py index 7d69e09b8da..ac1d1772825 100644 --- a/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py +++ b/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py @@ -126,10 +126,12 @@ async def get_marketplace(): # Allowlist for git-subdir paths: one or more segments separated by '/'. -# Each segment must start with an alphanumeric character and contain only -# alphanumeric characters, dots, hyphens, and underscores. -# This implicitly blocks '..', leading '/', backslashes, and percent-encoded sequences. -_VALID_GIT_SUBDIR_PATH_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*(/[a-zA-Z0-9][a-zA-Z0-9._-]*)*$") +# Each segment contains only alphanumeric characters, dots, hyphens, and +# underscores, and may start with a dot (e.g. '.claude/skills/my-skill'). +# The negative lookahead rejects the '.' and '..' segments so leading '/', +# backslashes, percent-encoded sequences, and path traversal are still blocked. +_GIT_SUBDIR_SEGMENT = r"(?!\.{1,2}(?:/|$))[a-zA-Z0-9._-]+" +_VALID_GIT_SUBDIR_PATH_RE = re.compile(rf"^{_GIT_SUBDIR_SEGMENT}(?:/{_GIT_SUBDIR_SEGMENT})*$") def _validate_plugin_source(source: Dict[str, Any]) -> None: diff --git a/tests/test_litellm/proxy/anthropic_endpoints/test_claude_code_marketplace.py b/tests/test_litellm/proxy/anthropic_endpoints/test_claude_code_marketplace.py index 1bdba166120..25f56243dca 100644 --- a/tests/test_litellm/proxy/anthropic_endpoints/test_claude_code_marketplace.py +++ b/tests/test_litellm/proxy/anthropic_endpoints/test_claude_code_marketplace.py @@ -170,6 +170,33 @@ async def test_register_plugin_git_subdir_empty_path(): assert "path" in exc_info.value.detail["error"] +@pytest.mark.asyncio +@pytest.mark.parametrize( + "path", + [ + ".claude/skills/my-skill", + ".github/workflows", + ".config", + "plugins/.hidden/my-plugin", + ], +) +async def test_register_plugin_git_subdir_dot_prefixed_path(path): + """git-subdir paths with dot-prefixed segments (e.g. .claude/) register successfully.""" + request = RegisterPluginRequest( + name="dot-prefixed-plugin", + source={ + "source": "git-subdir", + "url": "https://github.com/org/monorepo.git", + "path": path, + }, + ) + + response = await register_plugin(request=request, user_api_key_dict=_USER) + + assert response["status"] == "success" + assert response["plugin"]["source"]["path"] == path + + @pytest.mark.asyncio async def test_register_plugin_git_subdir_path_traversal(): """git-subdir with path traversal segments raises HTTP 400.""" @@ -181,6 +208,11 @@ async def test_register_plugin_git_subdir_path_traversal(): "plugins/%2e%2e/secrets", # percent-encoded traversal "plugins/%2E%2E/secrets", # uppercase percent-encoded traversal "plugins/%252e%252e/secrets", # double-encoded traversal + "..", # bare parent segment + ".", # bare current-dir segment + "plugins/..", # trailing parent traversal + ".claude/../secrets", # dot-prefixed segment followed by traversal + "plugins/./secrets", # embedded current-dir segment ]: request = RegisterPluginRequest( name="bad-plugin",