diff --git a/src/google/adk/code_executors/container_code_executor.py b/src/google/adk/code_executors/container_code_executor.py index d6a78d4d26..b1943924a6 100644 --- a/src/google/adk/code_executors/container_code_executor.py +++ b/src/google/adk/code_executors/container_code_executor.py @@ -37,6 +37,15 @@ class ContainerCodeExecutor(BaseCodeExecutor): """A code executor that uses a custom container to execute code. + Security note: this executor runs model-generated code, which may be + influenced by untrusted input (e.g. via prompt injection). By default the + container is started with networking disabled and all Linux capabilities + dropped so that the executed code cannot reach the network (including the + cloud metadata endpoint at ``169.254.169.254``) or escalate privileges. For + stronger, kernel-level isolation of untrusted code prefer + ``GkeCodeExecutor`` (gVisor) or a managed executor + (``VertexAiCodeExecutor`` / ``AgentEngineSandboxCodeExecutor``). + Attributes: base_url: Optional. The base url of the user hosted Docker client. image: The tag of the predefined image or custom image to run on the @@ -44,6 +53,9 @@ class ContainerCodeExecutor(BaseCodeExecutor): docker_path: The path to the directory containing the Dockerfile. If set, build the image from the dockerfile path instead of using the predefined image. Either docker_path or image must be set. + network_disabled: Whether to start the container with networking disabled. + Defaults to True. Set to False only if the executed code must make + network requests and you trust it. """ base_url: Optional[str] = None @@ -64,6 +76,17 @@ class ContainerCodeExecutor(BaseCodeExecutor): predefined image. Either docker_path or image must be set. """ + network_disabled: bool = True + """ + Whether to start the code execution container with networking disabled. + + Defaults to True so that untrusted, model-generated code cannot reach the + network -- in particular the cloud metadata endpoint at 169.254.169.254 + (which can yield the host's service-account credentials), internal services, + or arbitrary exfiltration destinations. Set to False only if the executed + code must make network requests and you trust it. + """ + # Overrides the BaseCodeExecutor attribute: this executor cannot be stateful. stateful: bool = Field(default=False, frozen=True, exclude=True) @@ -183,6 +206,13 @@ def __init_container(self): image=self.image, detach=True, tty=True, + # Harden the sandbox for untrusted, model-generated code: no network + # (blocks metadata/SSRF/exfil), drop all Linux capabilities, and + # forbid privilege escalation. Networking can be re-enabled via + # `network_disabled=False` when the executed code is trusted. + network_disabled=self.network_disabled, + cap_drop=['ALL'], + security_opt=['no-new-privileges'], ) logger.info('Container %s started.', self._container.id) diff --git a/tests/unittests/code_executors/test_container_code_executor.py b/tests/unittests/code_executors/test_container_code_executor.py new file mode 100644 index 0000000000..a26b02fdd2 --- /dev/null +++ b/tests/unittests/code_executors/test_container_code_executor.py @@ -0,0 +1,56 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the ContainerCodeExecutor container hardening defaults.""" + +from unittest import mock + +from google.adk.code_executors.container_code_executor import ContainerCodeExecutor + + +def _mock_docker_client(): + """Returns a mock Docker client whose container passes python verification.""" + client = mock.MagicMock() + container = mock.MagicMock() + # `_verify_python_installation` runs `exec_run(['which', 'python3'])` and + # checks `exit_code == 0`. + container.exec_run.return_value = mock.MagicMock(exit_code=0) + client.containers.run.return_value = container + return client + + +@mock.patch('google.adk.code_executors.container_code_executor.docker') +def test_container_is_hardened_by_default(mock_docker): + client = _mock_docker_client() + mock_docker.from_env.return_value = client + + ContainerCodeExecutor(image='test-image') + + _, kwargs = client.containers.run.call_args + # Untrusted model-generated code must not be able to reach the network + # (e.g. the cloud metadata endpoint) or escalate privileges by default. + assert kwargs['network_disabled'] is True + assert kwargs['cap_drop'] == ['ALL'] + assert kwargs['security_opt'] == ['no-new-privileges'] + + +@mock.patch('google.adk.code_executors.container_code_executor.docker') +def test_container_network_can_be_explicitly_enabled(mock_docker): + client = _mock_docker_client() + mock_docker.from_env.return_value = client + + ContainerCodeExecutor(image='test-image', network_disabled=False) + + _, kwargs = client.containers.run.call_args + assert kwargs['network_disabled'] is False