From 931c1c2fcd6b810206e198720f1c7ff73d91000e Mon Sep 17 00:00:00 2001 From: Nick Tindle Date: Wed, 11 Feb 2026 10:55:07 -0600 Subject: [PATCH] fix: Update test outputs for workspace_ref field - Add files output to ExecuteCodeBlock test_output - Add workspace_ref: None to ClaudeCodeBlock test expected output --- .../backend/backend/blocks/claude_code.py | 2 + .../backend/backend/blocks/code_executor.py | 1 + .../backend/test_disabled_block_bypass.py | 295 ++++++++++++++++++ .../ActivityDropdown.stories.tsx | 73 +++++ 4 files changed, 371 insertions(+) create mode 100644 autogpt_platform/backend/test_disabled_block_bypass.py create mode 100644 autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.stories.tsx diff --git a/autogpt_platform/backend/backend/blocks/claude_code.py b/autogpt_platform/backend/backend/blocks/claude_code.py index 311edf55b3..e76841f059 100644 --- a/autogpt_platform/backend/backend/blocks/claude_code.py +++ b/autogpt_platform/backend/backend/blocks/claude_code.py @@ -258,6 +258,7 @@ class ClaudeCodeBlock(Block): "relative_path": "index.html", "name": "index.html", "content": "Hello World", + "workspace_ref": None, } ], ), @@ -278,6 +279,7 @@ class ClaudeCodeBlock(Block): relative_path="index.html", name="index.html", content="Hello World", + workspace_ref=None, ) ], # files "User: Create a hello world HTML file\n" diff --git a/autogpt_platform/backend/backend/blocks/code_executor.py b/autogpt_platform/backend/backend/blocks/code_executor.py index 7ab5bf375c..38f06169d4 100644 --- a/autogpt_platform/backend/backend/blocks/code_executor.py +++ b/autogpt_platform/backend/backend/blocks/code_executor.py @@ -303,6 +303,7 @@ class ExecuteCodeBlock(Block, BaseE2BExecutorMixin): ("results", []), ("response", "Hello World"), ("stdout_logs", "Hello World\n"), + ("files", []), ], test_mock={ "execute_code": lambda api_key, code, language, template_id, setup_commands, timeout, dispose_sandbox, execution_context, extract_files: ( # noqa diff --git a/autogpt_platform/backend/test_disabled_block_bypass.py b/autogpt_platform/backend/test_disabled_block_bypass.py new file mode 100644 index 0000000000..97f024ba2e --- /dev/null +++ b/autogpt_platform/backend/test_disabled_block_bypass.py @@ -0,0 +1,295 @@ +""" +Test: GHSA-4crw-9p35-9x54 - Disabled block bypass via graph creation + +Validates that the fix prevents disabled blocks (specifically BlockInstallationBlock) +from being used in graphs. Tests all three attack vectors: + +1. Creating a graph with a disabled block +2. Executing a direct block call on a disabled block +3. Executing a graph containing a disabled block (if one existed pre-fix) + +Usage against a RUNNING server: + python test_disabled_block_bypass.py --base-url http://localhost:8006 --token + +Usage as a pytest unit test (no server needed): + poetry run pytest test_disabled_block_bypass.py -xvs +""" + +import argparse +import sys +import uuid + +import pytest + +# ── Block IDs of all currently disabled blocks ── +BLOCK_INSTALLATION_BLOCK_ID = "45e78db5-03e9-447f-9395-308d712f5f08" + + +# ═══════════════════════════════════════════════════════════════════════════════ +# UNIT TESTS — validate the fix at the model layer (no server needed) +# Run with: poetry run pytest test_disabled_block_bypass.py -xvs +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestDisabledBlockGraphValidation: + """Test that graph validation rejects disabled blocks.""" + + @staticmethod + def _make_graph_with_block(block_id: str, input_default: dict | None = None): + """Helper: build a GraphModel containing a single node with the given block.""" + from backend.data.graph import Graph, Node, make_graph_model + + node_id = str(uuid.uuid4()) + graph = Graph( + name=f"test-disabled-{block_id[:8]}", + description="GHSA-4crw-9p35-9x54 test", + nodes=[ + Node( + id=node_id, + block_id=block_id, + input_default=input_default or {}, + ) + ], + links=[], + ) + return make_graph_model(graph, user_id="test-user") + + def test_graph_with_block_installation_block_is_rejected(self): + """ + GHSA-4crw-9p35-9x54: The core vulnerability. + Creating a graph with BlockInstallationBlock (disabled=True) must fail validation. + """ + graph_model = self._make_graph_with_block( + BLOCK_INSTALLATION_BLOCK_ID, + input_default={"code": "print('should never run')"}, + ) + + with pytest.raises(ValueError, match="disabled"): + graph_model.validate_graph(for_run=False) + + def test_graph_with_block_installation_block_rejected_for_run(self): + """Same test but with for_run=True (execution-time validation).""" + graph_model = self._make_graph_with_block( + BLOCK_INSTALLATION_BLOCK_ID, + input_default={"code": "print('should never run')"}, + ) + + with pytest.raises(ValueError, match="disabled"): + graph_model.validate_graph(for_run=True) + + def test_all_disabled_blocks_are_rejected(self): + """Every block with disabled=True must be rejected in graph validation.""" + from backend.data.block import get_blocks + + blocks = get_blocks() + disabled_blocks = { + bid: block for bid, block in blocks.items() if block().disabled + } + + assert len(disabled_blocks) > 0, "Expected at least one disabled block" + print(f"\nFound {len(disabled_blocks)} disabled blocks to test:") + + for block_id, block_cls in disabled_blocks.items(): + block_inst = block_cls() + print(f" - {block_inst.name} ({block_id})") + + graph_model = self._make_graph_with_block(block_id) + + with pytest.raises(ValueError, match="disabled"): + graph_model.validate_graph(for_run=False) + + print(f"All {len(disabled_blocks)} disabled blocks correctly rejected!") + + def test_enabled_block_not_rejected_as_disabled(self): + """Sanity check: an enabled block should NOT be rejected for being disabled.""" + from backend.data.block import get_blocks + + blocks = get_blocks() + # Find an enabled block + enabled_entry = next( + (bid, b) for bid, b in blocks.items() if not b().disabled + ) + block_id, _ = enabled_entry + + graph_model = self._make_graph_with_block(block_id) + + # Should NOT raise "disabled" error. + # May raise other validation errors (missing inputs etc) which is fine. + try: + graph_model.validate_graph(for_run=False) + except ValueError as e: + assert "disabled" not in str(e).lower(), ( + f"Enabled block was incorrectly rejected as disabled: {e}" + ) + + def test_direct_block_execution_check(self): + """Verify BlockInstallationBlock is flagged disabled in the registry.""" + from backend.data.block import get_block + + block = get_block(BLOCK_INSTALLATION_BLOCK_ID) + assert block is not None, "BlockInstallationBlock not found in registry" + assert block.disabled is True, "BlockInstallationBlock should be disabled" + + def test_validate_graph_get_errors_returns_disabled_error(self): + """Test the structured error reporting path also catches disabled blocks.""" + graph_model = self._make_graph_with_block( + BLOCK_INSTALLATION_BLOCK_ID, + input_default={"code": "print('nope')"}, + ) + + # validate_graph_get_errors should return errors (not raise) for non-fatal issues, + # but disabled blocks raise ValueError directly in _validate_graph_get_errors + with pytest.raises(ValueError, match="disabled"): + graph_model.validate_graph_get_errors(for_run=False) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# INTEGRATION TESTS — validate against a running server +# Run with: python test_disabled_block_bypass.py --base-url URL --token JWT +# ═══════════════════════════════════════════════════════════════════════════════ + + +def run_against_server(base_url: str, token: str): + """ + Hit the actual API endpoints to confirm the fix works end-to-end. + Requires a running backend. + """ + import requests + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + results = [] + + print("=" * 70) + print("GHSA-4crw-9p35-9x54: Disabled Block Bypass - Integration Test") + print("=" * 70) + + # ── Test 1: Create graph with disabled BlockInstallationBlock ── + print("\n[TEST 1] POST /api/v1/graphs — graph with BlockInstallationBlock") + + node_id = str(uuid.uuid4()) + payload = { + "graph": { + "name": f"security-test-{uuid.uuid4().hex[:8]}", + "description": "GHSA-4crw-9p35-9x54 validation test", + "nodes": [ + { + "id": node_id, + "block_id": BLOCK_INSTALLATION_BLOCK_ID, + "input_default": { + "code": "# This should never be accepted\nprint('hello')" + }, + "metadata": {"position": {"x": 0, "y": 0}}, + } + ], + "links": [], + } + } + + resp = requests.post(f"{base_url}/api/v1/graphs", headers=headers, json=payload) + + if resp.status_code in (400, 422, 500): + print(f" PASS - Server rejected graph creation (HTTP {resp.status_code})") + try: + print(f" Detail: {resp.json().get('detail', resp.text)[:300]}") + except Exception: + print(f" Response: {resp.text[:300]}") + results.append(True) + elif resp.status_code in (200, 201): + print(f" FAIL - Server ACCEPTED the graph! (HTTP {resp.status_code})") + print(" >>> VULNERABILITY IS NOT FIXED <<<") + try: + graph_id = resp.json().get("id") + if graph_id: + requests.delete( + f"{base_url}/api/v1/graphs/{graph_id}", headers=headers + ) + print(f" (Cleaned up graph {graph_id})") + except Exception: + pass + results.append(False) + else: + print(f" WARN - Unexpected HTTP {resp.status_code}: {resp.text[:300]}") + results.append(None) + + # ── Test 2: Direct block execution of disabled block ── + print(f"\n[TEST 2] POST /api/v1/blocks/{BLOCK_INSTALLATION_BLOCK_ID}/execute") + + block_payload = { + "data": {"code": "# Should be rejected\nprint('hello')"}, + "input": {"code": "# Should be rejected\nprint('hello')"}, + } + + resp = requests.post( + f"{base_url}/api/v1/blocks/{BLOCK_INSTALLATION_BLOCK_ID}/execute", + headers=headers, + json=block_payload, + ) + + if resp.status_code == 403: + print(f" PASS - Server rejected direct execution (HTTP 403)") + try: + print(f" Detail: {resp.json().get('detail', '')}") + except Exception: + pass + results.append(True) + elif resp.status_code == 200: + print(f" FAIL - Server EXECUTED the disabled block!") + print(" >>> VULNERABILITY IS NOT FIXED <<<") + results.append(False) + else: + print(f" INFO - HTTP {resp.status_code}: {resp.text[:300]}") + results.append(None) + + # ── Test 3: External API direct block execution ── + print( + f"\n[TEST 3] POST /api/external/v1/blocks/{BLOCK_INSTALLATION_BLOCK_ID}/execute" + ) + + resp = requests.post( + f"{base_url}/api/external/v1/blocks/{BLOCK_INSTALLATION_BLOCK_ID}/execute", + headers=headers, + json=block_payload, + ) + + if resp.status_code in (403, 401): + print(f" PASS - Server rejected (HTTP {resp.status_code})") + results.append(True) + elif resp.status_code == 200: + print(f" FAIL - External API EXECUTED the disabled block!") + results.append(False) + else: + print(f" INFO - HTTP {resp.status_code} (may need API key auth for this endpoint)") + results.append(None) + + # ── Summary ── + passed = sum(1 for r in results if r is True) + failed = sum(1 for r in results if r is False) + + print("\n" + "=" * 70) + if failed > 0: + print(f"RESULT: {failed} test(s) FAILED — vulnerability may not be fixed!") + else: + print(f"RESULT: {passed}/{len(results)} tests passed — fix is working.") + print("=" * 70) + + return failed == 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Test GHSA-4crw-9p35-9x54 fix (disabled block bypass)" + ) + parser.add_argument( + "--base-url", + default="http://localhost:8006", + help="Backend base URL (default: http://localhost:8006)", + ) + parser.add_argument("--token", required=True, help="JWT bearer token") + args = parser.parse_args() + + success = run_against_server(args.base_url, args.token) + sys.exit(0 if success else 1) diff --git a/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.stories.tsx b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.stories.tsx new file mode 100644 index 0000000000..9f96591b0c --- /dev/null +++ b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.stories.tsx @@ -0,0 +1,73 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { ActivityDropdown } from "./ActivityDropdown"; + +const mockExecutions = [ + { + type: "running" as const, + agent_name: "Web Scraper Agent", + agent_description: "Scrapes data from websites", + execution_id: "exec-1", + started_at: new Date().toISOString(), + stats: { nodes_total: 10, nodes_completed: 5, nodes_failed: 0 }, + }, + { + type: "completed" as const, + agent_name: "Data Analyzer", + agent_description: "Analyzes datasets", + execution_id: "exec-2", + started_at: new Date(Date.now() - 3600000).toISOString(), + ended_at: new Date().toISOString(), + stats: { nodes_total: 8, nodes_completed: 8, nodes_failed: 0 }, + }, + { + type: "failed" as const, + agent_name: "Email Sender", + agent_description: "Sends automated emails", + execution_id: "exec-3", + started_at: new Date(Date.now() - 7200000).toISOString(), + ended_at: new Date(Date.now() - 3600000).toISOString(), + stats: { nodes_total: 5, nodes_completed: 3, nodes_failed: 2 }, + }, +]; + +const meta: Meta = { + title: "Layout/Navbar/ActivityDropdown", + component: ActivityDropdown, + parameters: { + layout: "centered", + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const WithActivity: Story = { + args: { + activeExecutions: [mockExecutions[0]] as any, + recentCompletions: [mockExecutions[1]] as any, + recentFailures: [mockExecutions[2]] as any, + }, +}; + +export const Empty: Story = { + args: { + activeExecutions: [], + recentCompletions: [], + recentFailures: [], + }, +}; + +export const ManyItems: Story = { + args: { + activeExecutions: Array(5).fill(mockExecutions[0]).map((e, i) => ({ ...e, execution_id: `running-${i}` })) as any, + recentCompletions: Array(10).fill(mockExecutions[1]).map((e, i) => ({ ...e, execution_id: `completed-${i}` })) as any, + recentFailures: Array(3).fill(mockExecutions[2]).map((e, i) => ({ ...e, execution_id: `failed-${i}` })) as any, + }, +};