From de5b6e8bd4dccc866ab387f50f87425d63ffe78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adil=20Burak=20=C5=9EEN?= Date: Thu, 11 Jun 2026 03:55:55 +0300 Subject: [PATCH] fix(code_executors): harden ContainerCodeExecutor sandbox by default ContainerCodeExecutor runs model-generated code, which can be influenced by untrusted input (e.g. via prompt injection). It previously started the container with default Docker networking and no capability restrictions, so the executed code could reach the cloud metadata endpoint (169.254.169.254) and exfiltrate the host service-account credentials, reach internal services, or escalate privileges. Start the container with networking disabled (configurable via a new `network_disabled` field, default True), drop all Linux capabilities, and forbid privilege escalation -- aligning with the isolation posture of GkeCodeExecutor and the managed executors. Add unit tests covering the hardened defaults and the opt-in network path. --- .../code_executors/container_code_executor.py | 30 ++++++++++ .../test_container_code_executor.py | 56 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 tests/unittests/code_executors/test_container_code_executor.py 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