Skip to content

Commit 5257869

Browse files
GWealecopybara-github
authored andcommitted
fix: Redact sensitive information from URIs in logs
This change introduces a helper function `_redact_uri_for_log` to sanitize URIs before logging. It removes user credentials from the netloc and redacts the values of query parameters, ensuring that sensitive information like passwords is not exposed in log outputs. The function is applied to all log statements and error messages that include service URIs for session, memory, and artifact services Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 858703465
1 parent 53b67ce commit 5257869

File tree

2 files changed

+147
-14
lines changed

2 files changed

+147
-14
lines changed

src/google/adk/cli/utils/service_factory.py

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
from pathlib import Path
2020
from typing import Any
2121
from typing import Optional
22+
from urllib.parse import parse_qsl
23+
from urllib.parse import urlsplit
24+
from urllib.parse import urlunsplit
2225

2326
from ...artifacts.base_artifact_service import BaseArtifactService
2427
from ...memory.base_memory_service import BaseMemoryService
@@ -42,6 +45,41 @@
4245
_KUBERNETES_HOST_ENV = "KUBERNETES_SERVICE_HOST"
4346

4447

48+
def _redact_uri_for_log(uri: str) -> str:
49+
"""Returns a safe-to-log representation of a URI.
50+
51+
Redacts user info (username/password) and query parameter values.
52+
"""
53+
if not uri or not uri.strip():
54+
return "<empty>"
55+
sanitized = uri.replace("\r", "\\r").replace("\n", "\\n")
56+
if "://" not in sanitized:
57+
return "<scheme-missing>"
58+
try:
59+
parsed = urlsplit(sanitized)
60+
except ValueError:
61+
return "<unparseable>"
62+
63+
if not parsed.scheme:
64+
return "<scheme-missing>"
65+
66+
netloc = parsed.netloc
67+
if "@" in netloc:
68+
_, netloc = netloc.rsplit("@", 1)
69+
70+
if parsed.query:
71+
try:
72+
redacted_pairs = parse_qsl(parsed.query, keep_blank_values=True)
73+
except ValueError:
74+
query = "<redacted>"
75+
else:
76+
query = "&".join(f"{key}=<redacted>" for key, _ in redacted_pairs)
77+
else:
78+
query = ""
79+
80+
return urlunsplit((parsed.scheme, netloc, parsed.path, query, ""))
81+
82+
4583
def _is_cloud_run() -> bool:
4684
"""Returns True when running in Cloud Run."""
4785
return bool(os.environ.get(_CLOUD_RUN_SERVICE_ENV))
@@ -148,7 +186,10 @@ def create_session_service_from_options(
148186
kwargs.update(session_db_kwargs)
149187

150188
if session_service_uri:
151-
logger.info("Using session service URI: %s", session_service_uri)
189+
logger.info(
190+
"Using session service URI: %s",
191+
_redact_uri_for_log(session_service_uri),
192+
)
152193
service = registry.create_session_service(session_service_uri, **kwargs)
153194
if service is not None:
154195
return service
@@ -162,7 +203,7 @@ def create_session_service_from_options(
162203
fallback_kwargs.pop("agents_dir", None)
163204
logger.info(
164205
"Falling back to DatabaseSessionService for URI: %s",
165-
session_service_uri,
206+
_redact_uri_for_log(session_service_uri),
166207
)
167208
return DatabaseSessionService(db_url=session_service_uri, **fallback_kwargs)
168209

@@ -208,13 +249,18 @@ def create_memory_service_from_options(
208249
registry = get_service_registry()
209250

210251
if memory_service_uri:
211-
logger.info("Using memory service URI: %s", memory_service_uri)
252+
logger.info(
253+
"Using memory service URI: %s", _redact_uri_for_log(memory_service_uri)
254+
)
212255
service = registry.create_memory_service(
213256
memory_service_uri,
214257
agents_dir=str(base_path),
215258
)
216259
if service is None:
217-
raise ValueError(f"Unsupported memory service URI: {memory_service_uri}")
260+
raise ValueError(
261+
"Unsupported memory service URI: %s"
262+
% _redact_uri_for_log(memory_service_uri)
263+
)
218264
return service
219265

220266
logger.info("Using in-memory memory service")
@@ -235,19 +281,23 @@ def create_artifact_service_from_options(
235281
registry = get_service_registry()
236282

237283
if artifact_service_uri:
238-
logger.info("Using artifact service URI: %s", artifact_service_uri)
284+
logger.info(
285+
"Using artifact service URI: %s",
286+
_redact_uri_for_log(artifact_service_uri),
287+
)
239288
service = registry.create_artifact_service(
240289
artifact_service_uri,
241290
agents_dir=str(base_path),
242291
)
243292
if service is None:
244293
if strict_uri:
245294
raise ValueError(
246-
f"Unsupported artifact service URI: {artifact_service_uri}"
295+
"Unsupported artifact service URI: %s"
296+
% _redact_uri_for_log(artifact_service_uri)
247297
)
248298
return _create_in_memory_artifact_service(
249299
"Unsupported artifact service URI: %s, falling back to in-memory",
250-
artifact_service_uri,
300+
_redact_uri_for_log(artifact_service_uri),
251301
)
252302
return service
253303

tests/unittests/cli/utils/test_service_factory.py

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616

1717
from __future__ import annotations
1818

19+
import logging
1920
import os
2021
from pathlib import Path
21-
from unittest.mock import Mock
22+
from unittest import mock
2223

2324
from google.adk.artifacts.file_artifact_service import FileArtifactService
2425
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
26+
from google.adk.cli.service_registry import ServiceRegistry
2527
from google.adk.cli.utils.local_storage import PerAgentDatabaseSessionService
2628
import google.adk.cli.utils.service_factory as service_factory
2729
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
@@ -31,7 +33,7 @@
3133

3234

3335
def test_create_session_service_uses_registry(tmp_path: Path, monkeypatch):
34-
registry = Mock()
36+
registry = mock.create_autospec(ServiceRegistry, instance=True, spec_set=True)
3537
expected = object()
3638
registry.create_session_service.return_value = expected
3739
monkeypatch.setattr(service_factory, "get_service_registry", lambda: registry)
@@ -48,6 +50,87 @@ def test_create_session_service_uses_registry(tmp_path: Path, monkeypatch):
4850
)
4951

5052

53+
def test_create_session_service_logs_redacted_uri(
54+
tmp_path: Path,
55+
monkeypatch: pytest.MonkeyPatch,
56+
caplog: pytest.LogCaptureFixture,
57+
) -> None:
58+
registry = mock.create_autospec(ServiceRegistry, instance=True, spec_set=True)
59+
registry.create_session_service.return_value = object()
60+
monkeypatch.setattr(service_factory, "get_service_registry", lambda: registry)
61+
62+
session_service_uri = (
63+
"postgresql://user:supersecret@localhost:5432/dbname?sslmode=require"
64+
)
65+
caplog.set_level(logging.INFO, logger=service_factory.logger.name)
66+
67+
service_factory.create_session_service_from_options(
68+
base_dir=tmp_path,
69+
session_service_uri=session_service_uri,
70+
)
71+
72+
assert "supersecret" not in caplog.text
73+
assert "sslmode=require" not in caplog.text
74+
assert "localhost:5432" in caplog.text
75+
76+
77+
def test_redact_uri_for_log_removes_credentials_with_at_in_password() -> None:
78+
uri = "postgresql://user:super@secret@localhost:5432/dbname"
79+
80+
assert (
81+
service_factory._redact_uri_for_log(uri)
82+
== "postgresql://localhost:5432/dbname"
83+
)
84+
85+
86+
def test_redact_uri_for_log_preserves_host_when_no_credentials() -> None:
87+
uri = "postgresql://localhost:5432/dbname?sslmode=require&password=secret"
88+
89+
redacted = service_factory._redact_uri_for_log(uri)
90+
91+
assert redacted.startswith("postgresql://localhost:5432/dbname?")
92+
assert "require" not in redacted
93+
assert "secret" not in redacted
94+
assert "sslmode=<redacted>" in redacted
95+
assert "password=<redacted>" in redacted
96+
97+
98+
def test_redact_uri_for_log_redacts_when_parse_qsl_fails(
99+
monkeypatch: pytest.MonkeyPatch,
100+
) -> None:
101+
def _raise_value_error(*_args, **_kwargs):
102+
raise ValueError("bad query")
103+
104+
monkeypatch.setattr(service_factory, "parse_qsl", _raise_value_error)
105+
106+
uri = "postgresql://user:pass@localhost:5432/dbname?sslmode=require"
107+
redacted = service_factory._redact_uri_for_log(uri)
108+
109+
assert "pass" not in redacted
110+
assert "require" not in redacted
111+
assert redacted.endswith("?<redacted>")
112+
113+
114+
def test_redact_uri_for_log_escapes_crlf() -> None:
115+
uri = (
116+
"postgresql://user:pass@localhost:5432/dbname\rINJECT\nINJECT"
117+
"?sslmode=require"
118+
)
119+
120+
redacted = service_factory._redact_uri_for_log(uri)
121+
122+
assert "\r" not in redacted
123+
assert "\n" not in redacted
124+
assert "\\rINJECT\\nINJECT" in redacted
125+
126+
127+
def test_redact_uri_for_log_returns_scheme_missing_without_separator() -> None:
128+
assert (
129+
service_factory._redact_uri_for_log("user:pass@localhost:5432/dbname")
130+
== "<scheme-missing>"
131+
)
132+
133+
51134
@pytest.mark.asyncio
52135
async def test_create_session_service_defaults_to_per_agent_sqlite(
53136
tmp_path: Path,
@@ -88,7 +171,7 @@ async def test_create_session_service_respects_app_name_mapping(
88171
def test_create_session_service_fallbacks_to_database(
89172
tmp_path: Path, monkeypatch
90173
):
91-
registry = Mock()
174+
registry = mock.create_autospec(ServiceRegistry, instance=True, spec_set=True)
92175
registry.create_session_service.return_value = None
93176
monkeypatch.setattr(service_factory, "get_service_registry", lambda: registry)
94177

@@ -109,7 +192,7 @@ def test_create_session_service_fallbacks_to_database(
109192

110193

111194
def test_create_artifact_service_uses_registry(tmp_path: Path, monkeypatch):
112-
registry = Mock()
195+
registry = mock.create_autospec(ServiceRegistry, instance=True, spec_set=True)
113196
expected = object()
114197
registry.create_artifact_service.return_value = expected
115198
monkeypatch.setattr(service_factory, "get_service_registry", lambda: registry)
@@ -129,7 +212,7 @@ def test_create_artifact_service_uses_registry(tmp_path: Path, monkeypatch):
129212
def test_create_artifact_service_raises_on_unknown_scheme_when_strict(
130213
tmp_path: Path, monkeypatch
131214
):
132-
registry = Mock()
215+
registry = mock.create_autospec(ServiceRegistry, instance=True, spec_set=True)
133216
registry.create_artifact_service.return_value = None
134217
monkeypatch.setattr(service_factory, "get_service_registry", lambda: registry)
135218

@@ -142,7 +225,7 @@ def test_create_artifact_service_raises_on_unknown_scheme_when_strict(
142225

143226

144227
def test_create_memory_service_uses_registry(tmp_path: Path, monkeypatch):
145-
registry = Mock()
228+
registry = mock.create_autospec(ServiceRegistry, instance=True, spec_set=True)
146229
expected = object()
147230
registry.create_memory_service.return_value = expected
148231
monkeypatch.setattr(service_factory, "get_service_registry", lambda: registry)
@@ -170,7 +253,7 @@ def test_create_memory_service_defaults_to_in_memory(tmp_path: Path):
170253
def test_create_memory_service_raises_on_unknown_scheme(
171254
tmp_path: Path, monkeypatch
172255
):
173-
registry = Mock()
256+
registry = mock.create_autospec(ServiceRegistry, instance=True, spec_set=True)
174257
registry.create_memory_service.return_value = None
175258
monkeypatch.setattr(service_factory, "get_service_registry", lambda: registry)
176259

0 commit comments

Comments
 (0)