CVE-2025-66454
Description
Arcade MCP allows you to to create, deploy, and share MCP Servers. Prior to 1.5.4, the arcade-mcp HTTP server uses a hardcoded default worker secret ("dev") that is never validated or overridden during normal server startup. As a result, any unauthenticated attacker who knows this default key can forge valid JWTs and fully bypass the FastAPI authentication layer. This grants remote access to all worker endpoints—including tool enumeration and tool invocation—without credentials. This vulnerability is fixed in 1.5.4.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
arcade-mcp-serverPyPI | < 1.9.1 | 1.9.1 |
Affected products
1- Range: 0.0.10, 0.0.11, 0.0.12, …
Patches
27fb097f20fbeUse monkeypatch for tests that use ARCADE_WORKER_SECRET (#694)
4 files changed · +12 −33
libs/arcade-mcp-server/pyproject.toml+1 −1 modified@@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "arcade-mcp-server" -version = "1.9.0" +version = "1.9.1" description = "Model Context Protocol (MCP) server framework for Arcade.dev" readme = "README.md" authors = [{ name = "Arcade.dev" }]
libs/tests/worker/test_worker_base.py+7 −28 modified@@ -43,18 +43,11 @@ def mock_router(): @pytest.fixture -def base_worker(mock_router): - # Save original value - original_secret = os.environ.get("ARCADE_WORKER_SECRET") +def base_worker(mock_router, monkeypatch): # Set env var temporarily for testing secret loading - os.environ["ARCADE_WORKER_SECRET"] = "test_secret_env" # noqa: S105 + monkeypatch.setenv("ARCADE_WORKER_SECRET", "test_secret_env") worker = BaseWorker() worker.register_routes(mock_router) # Register routes using the mock router - # Restore original value - if original_secret is not None: - os.environ["ARCADE_WORKER_SECRET"] = original_secret - else: - del os.environ["ARCADE_WORKER_SECRET"] return worker @@ -72,33 +65,19 @@ def test_base_worker_init_with_secret(): assert not worker.disable_auth -def test_base_worker_init_with_env_secret(): - original_secret = os.environ.get("ARCADE_WORKER_SECRET") - os.environ["ARCADE_WORKER_SECRET"] = "env_secret_value" # noqa: S105 +def test_base_worker_init_with_env_secret(monkeypatch): + monkeypatch.setenv("ARCADE_WORKER_SECRET", "env_secret_value") worker = BaseWorker() assert worker.secret == "env_secret_value" # noqa: S105 assert not worker.disable_auth - # Restore secret to original if it was set - if original_secret is not None: - os.environ["ARCADE_WORKER_SECRET"] = original_secret - else: - del os.environ["ARCADE_WORKER_SECRET"] - - -def test_base_worker_init_no_secret_raises_error(): - # Ensure secret is not set - original_secret = os.environ.get("ARCADE_WORKER_SECRET") - if "ARCADE_WORKER_SECRET" in os.environ: - del os.environ["ARCADE_WORKER_SECRET"] +def test_base_worker_init_no_secret_raises_error(monkeypatch): + # Ensure env var is not set + monkeypatch.delenv("ARCADE_WORKER_SECRET", raising=False) with pytest.raises(ValueError, match="No secret provided for worker"): BaseWorker() - # Restore secret if it was set - if original_secret is not None: - os.environ["ARCADE_WORKER_SECRET"] = original_secret - def test_base_worker_init_disable_auth(): worker = BaseWorker(disable_auth=True)
libs/tests/worker/test_worker_fastapi.py+3 −3 modified@@ -100,8 +100,8 @@ def test_health_check_route_no_auth(client_no_auth): # Catalog def test_get_catalog_route_no_auth_header(client): response = client.get("/worker/tools") - assert response.status_code == 403 - assert "Not authenticated" in response.text + assert response.status_code in [403, 401] + assert "Not authenticated" in response.text or "Unauthorized" in response.text def test_get_catalog_route_invalid_auth_header(client, worker_secret): @@ -131,7 +131,7 @@ def call_tool_payload(): def test_call_tool_route_no_auth_header(client, call_tool_payload): response = client.post("/worker/tools/invoke", json=call_tool_payload) - assert response.status_code == 403 + assert response.status_code in [403, 401] def test_call_tool_route_invalid_auth_header(client, worker_secret, call_tool_payload):
pyproject.toml+1 −1 modified@@ -1,6 +1,6 @@ [project] name = "arcade-mcp" -version = "1.5.4" +version = "1.5.6" description = "Arcade.dev - Tool Calling platform for Agents" readme = "README.md" license = {file = "LICENSE"}
44660d18ceb2Only serve worker endpoints if secret is set (#691)
7 files changed · +38 −18
libs/arcade-cli/arcade_cli/deploy.py+1 −0 modified@@ -383,6 +383,7 @@ def start_server_process(entrypoint: str, debug: bool = False) -> tuple[subproce "ARCADE_SERVER_PORT": str(port), "ARCADE_SERVER_TRANSPORT": "http", "ARCADE_AUTH_DISABLED": "true", + "ARCADE_WORKER_SECRET": "temp-validation-secret", } cmd = [sys.executable, entrypoint]
libs/arcade-mcp-server/arcade_mcp_server/settings.py+2 −2 modified@@ -142,8 +142,8 @@ class ArcadeSettings(BaseSettings): description="Disable authentication", ) server_secret: str | None = Field( - default="dev", - description="Server secret", + default=None, + description="Server secret for worker endpoints (required to enable worker routes)", validation_alias="ARCADE_WORKER_SECRET", ) environment: str = Field(
libs/arcade-mcp-server/arcade_mcp_server/worker.py+11 −10 modified@@ -123,15 +123,14 @@ def create_arcade_mcp( **kwargs: Any, ) -> FastAPI: """ - Create a FastAPI app exposing Arcade Worker and MCP HTTP endpoints. + Create a FastAPI app exposing MCP HTTP endpoints + and Arcade Worker endpoints if a secret is provided. MCP is always enabled in this integrated application. """ if mcp_settings is None: mcp_settings = MCPSettings.from_env() secret = mcp_settings.arcade.server_secret - if secret is None: - secret = "dev" # noqa: S105 otel_handler = OTELHandler( enable=otel_enable, @@ -180,13 +179,15 @@ async def track_tasks_middleware( app.add_middleware(AddTrailingSlashToPathMiddleware) # Worker endpoints - worker = FastAPIWorker( - app=app, - secret=secret, - disable_auth=mcp_settings.arcade.auth_disabled, - otel_meter=otel_handler.get_meter(), - ) - worker.catalog = catalog + if secret is not None: + worker = FastAPIWorker( + app=app, + secret=secret, + disable_auth=mcp_settings.arcade.auth_disabled, + otel_meter=otel_handler.get_meter(), + ) + worker.catalog = catalog + logger.info("Worker routes enabled at /worker/* (ARCADE_WORKER_SECRET is set)") class _MCPASGIProxy: def __init__(self, parent_app: FastAPI):
libs/arcade-mcp-server/pyproject.toml+1 −1 modified@@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "arcade-mcp-server" -version = "1.8.0" +version = "1.9.0" description = "Model Context Protocol (MCP) server framework for Arcade.dev" readme = "README.md" authors = [{ name = "Arcade.dev" }]
libs/tests/arcade_mcp_server/integration/test_end_to_end.py+1 −0 modified@@ -64,6 +64,7 @@ def start_mcp_server( "ARCADE_SERVER_PORT": str(port), "ARCADE_SERVER_TRANSPORT": "http", "ARCADE_AUTH_DISABLED": "true", + "ARCADE_WORKER_SECRET": "test-secret-e2e", } cmd = ["uv", "run", entrypoint_path, "http"]
libs/tests/worker/test_worker_base.py+21 −4 modified@@ -44,12 +44,17 @@ def mock_router(): @pytest.fixture def base_worker(mock_router): + # Save original value + original_secret = os.environ.get("ARCADE_WORKER_SECRET") # Set env var temporarily for testing secret loading os.environ["ARCADE_WORKER_SECRET"] = "test_secret_env" # noqa: S105 worker = BaseWorker() worker.register_routes(mock_router) # Register routes using the mock router - # Clean up env var - del os.environ["ARCADE_WORKER_SECRET"] + # Restore original value + if original_secret is not None: + os.environ["ARCADE_WORKER_SECRET"] = original_secret + else: + del os.environ["ARCADE_WORKER_SECRET"] return worker @@ -68,20 +73,32 @@ def test_base_worker_init_with_secret(): def test_base_worker_init_with_env_secret(): + original_secret = os.environ.get("ARCADE_WORKER_SECRET") os.environ["ARCADE_WORKER_SECRET"] = "env_secret_value" # noqa: S105 worker = BaseWorker() assert worker.secret == "env_secret_value" # noqa: S105 assert not worker.disable_auth - del os.environ["ARCADE_WORKER_SECRET"] + + # Restore secret to original if it was set + if original_secret is not None: + os.environ["ARCADE_WORKER_SECRET"] = original_secret + else: + del os.environ["ARCADE_WORKER_SECRET"] def test_base_worker_init_no_secret_raises_error(): - # Ensure env var is not set + # Ensure secret is not set + original_secret = os.environ.get("ARCADE_WORKER_SECRET") if "ARCADE_WORKER_SECRET" in os.environ: del os.environ["ARCADE_WORKER_SECRET"] + with pytest.raises(ValueError, match="No secret provided for worker"): BaseWorker() + # Restore secret if it was set + if original_secret is not None: + os.environ["ARCADE_WORKER_SECRET"] = original_secret + def test_base_worker_init_disable_auth(): worker = BaseWorker(disable_auth=True)
pyproject.toml+1 −1 modified@@ -1,6 +1,6 @@ [project] name = "arcade-mcp" -version = "1.5.3" +version = "1.5.4" description = "Arcade.dev - Tool Calling platform for Agents" readme = "README.md" license = {file = "LICENSE"}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-g2jx-37x6-6438ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-66454ghsaADVISORY
- github.com/ArcadeAI/arcade-mcp/commit/44660d18ceb220600401303df860a31ca766c817nvdWEB
- github.com/ArcadeAI/arcade-mcp/commit/7fb097f20fbea35e382a1b78da6fd90609c55a9eghsaWEB
- github.com/ArcadeAI/arcade-mcp/pull/691nvdWEB
- github.com/ArcadeAI/arcade-mcp/security/advisories/GHSA-g2jx-37x6-6438nvdWEB
News mentions
0No linked articles in our index yet.