From 2783b16bcc9b33a9bcb3d109e72ac3dd295aa50e Mon Sep 17 00:00:00 2001 From: Tommaso Bona Date: Wed, 19 Aug 2026 12:49:05 +0200 Subject: [PATCH 1/3] Implement HTML artifact sanitization Add a function to sanitize HTML artifacts before returning them. --- src/google/adk/cli/api_server.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/google/adk/cli/api_server.py b/src/google/adk/cli/api_server.py index b61599f6f6..71c724c644 100644 --- a/src/google/adk/cli/api_server.py +++ b/src/google/adk/cli/api_server.py @@ -51,6 +51,7 @@ from fastapi.websockets import WebSocket from fastapi.websockets import WebSocketDisconnect from google.genai import types +import nh3 from opentelemetry import trace import opentelemetry.sdk.environment_variables as otel_env from opentelemetry.sdk.trace import export as export_lib @@ -105,6 +106,21 @@ _REGEX_PREFIX = "regex:" +def _sanitize_html_artifact(artifact: types.Part) -> types.Part: + """Sanitize HTML artifact data before returning it to a browser.""" + inline_data = artifact.inline_data + if not inline_data or inline_data.mime_type != "text/html": + return artifact + + try: + html = inline_data.data.decode("utf-8") + except UnicodeDecodeError as exc: + raise HTTPException(status_code=422, detail="Invalid HTML encoding") from exc + + inline_data.data = nh3.clean(html).encode("utf-8") + return artifact + + def _parse_cors_origins( allow_origins: list[str], ) -> tuple[list[str], Optional[str]]: @@ -1684,7 +1700,7 @@ async def load_artifact_version( ) if not artifact: raise HTTPException(status_code=404, detail="Artifact not found") - return artifact + return _sanitize_html_artifact(artifact) @app.get( "/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts", @@ -1734,7 +1750,7 @@ async def load_artifact( ) if not artifact: raise HTTPException(status_code=404, detail="Artifact not found") - return artifact + return _sanitize_html_artifact(artifact) @app.delete( "/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name:path}", From 23914383a35a7bbe9fd34ab30b2d8b22208de971 Mon Sep 17 00:00:00 2001 From: Tommaso Bona Date: Wed, 19 Aug 2026 12:50:25 +0200 Subject: [PATCH 2/3] Add nh3 dependency to pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 46edcbc694..5250591d8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "graphviz>=0.20.2,<1", "httpx>=0.27,<1", "jsonschema>=4.23,<5", + "nh3>=0.3,<1", "opentelemetry-api>=1.39,<=1.42.1", "opentelemetry-sdk>=1.39,<=1.42.1", "packaging>=21", From 1e51e47af07b127d09b7a386ba6b1bf7d579cbbb Mon Sep 17 00:00:00 2001 From: Tommaso Bona Date: Wed, 19 Aug 2026 12:54:01 +0200 Subject: [PATCH 3/3] Create unittests for HTML sanitization Add unit tests for HTML sanitization to block XSS and preserve safe HTML. --- tests/unittests/cli/test_html_sanitization.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/unittests/cli/test_html_sanitization.py diff --git a/tests/unittests/cli/test_html_sanitization.py b/tests/unittests/cli/test_html_sanitization.py new file mode 100644 index 0000000000..1f2ca6787e --- /dev/null +++ b/tests/unittests/cli/test_html_sanitization.py @@ -0,0 +1,62 @@ +# Copyright 2026 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. + +from __future__ import annotations + +from google.adk.cli.api_server import _sanitize_html_artifact +from google.genai import types +import pytest + + +@pytest.mark.parametrize( + "payload", + ( + "", + '', + '', + '', + 'click', + '', + ), +) +def test_sanitize_html_artifact_blocks_xss(payload: str) -> None: + artifact = types.Part.from_bytes(data=payload.encode(), mime_type="text/html") + + sanitized = _sanitize_html_artifact(artifact).inline_data.data.decode() + + assert "alert" not in sanitized + assert "script" not in sanitized + assert "onerror" not in sanitized + assert "onload" not in sanitized + assert "iframe" not in sanitized + assert "javascript:" not in sanitized + + +def test_sanitize_html_artifact_preserves_safe_html() -> None: + artifact = types.Part.from_bytes( + data=b"

Title

Hello world

", + mime_type="text/html", + ) + + sanitized = _sanitize_html_artifact(artifact).inline_data.data.decode() + + assert "

Title

" in sanitized + assert "world" in sanitized + + +def test_sanitize_html_artifact_ignores_other_mime_types() -> None: + artifact = types.Part.from_bytes(data=b"unchanged", mime_type="text/plain") + + assert _sanitize_html_artifact(artifact) is artifact + assert artifact.inline_data.data == b"unchanged"