diff --git a/homeassistant/components/go2rtc/server.py b/homeassistant/components/go2rtc/server.py index e386d0677b23..7533d3dc6c03 100644 --- a/homeassistant/components/go2rtc/server.py +++ b/homeassistant/components/go2rtc/server.py @@ -4,6 +4,7 @@ import asyncio from collections import deque from contextlib import suppress import logging +import re from tempfile import NamedTemporaryFile from aiohttp import ClientSession @@ -105,6 +106,21 @@ _LOG_LEVEL_MAP = { } +# go2rtc logs stream urls verbatim, which may embed camera credentials. +# Upstream strips the url userinfo since AlexxIT/go2rtc#2051, but no release +# includes it yet (v1.9.14 is the newest). It does not touch query parameters. +_URL_USERINFO = re.compile(r"://[^/?#\s@]+@") +_URL_CREDENTIAL_QUERY = re.compile( + r"([?&](?:auth|user|password)=)[^&\s]+", re.IGNORECASE +) + + +def _redact_url_credentials(msg: str) -> str: + """Redact credentials from urls in a go2rtc log message.""" + msg = _URL_USERINFO.sub("://****@", msg) + return _URL_CREDENTIAL_QUERY.sub(r"\1****", msg) + + class Go2RTCServerStartError(HomeAssistantError): """Raised when server does not start.""" @@ -238,7 +254,7 @@ class Server: assert process.stdout is not None async for line in process.stdout: - msg = line[:-1].decode().strip() + msg = _redact_url_credentials(line[:-1].decode().strip()) self._log_buffer.append(msg) loglevel = logging.WARNING if len(split_msg := msg.split(" ", 2)) == 3: diff --git a/tests/components/go2rtc/test_server.py b/tests/components/go2rtc/test_server.py index 36319b44487f..fd9251a7f1c5 100644 --- a/tests/components/go2rtc/test_server.py +++ b/tests/components/go2rtc/test_server.py @@ -425,3 +425,48 @@ async def test_server_restart_error( assert "Unexpected error when restarting go2rtc server" in caplog.text await server.stop() + + +@pytest.mark.parametrize( + ("server_stdout", "expected_stdout"), + [ + ( + [ + "09:00:03.467 INF [api] listen addr=127.0.0.1:1984", + "10:27:02.622 WRN producer.go:170 >" + ' error="read tcp 192.168.1.96:42550->192.168.1.145:554: i/o timeout"' + " url=rtsp://admin:hunter2@192.168.1.145:554/Preview_01_sub", + "10:27:02.623 WRN [streams] url=http://192.168.1.145/snapshot" + "?auth=token&channel=0&user=admin&password=hunter2", + "10:27:02.624 WRN [streams] url=http://camera.local" + "?user=alice@example.com", + ], + [ + "10:27:02.622 WRN producer.go:170 >" + ' error="read tcp 192.168.1.96:42550->192.168.1.145:554: i/o timeout"' + " url=rtsp://****@192.168.1.145:554/Preview_01_sub", + "10:27:02.623 WRN [streams] url=http://192.168.1.145/snapshot" + "?auth=****&channel=0&user=****&password=****", + "10:27:02.624 WRN [streams] url=http://camera.local?user=****", + ], + ) + ], +) +@pytest.mark.usefixtures("mock_tempfile", "rest_client") +async def test_credentials_redacted_from_server_output( + hass: HomeAssistant, + mock_create_subprocess: MagicMock, + server: Server, + caplog: pytest.LogCaptureFixture, + expected_stdout: list[str], +) -> None: + """Test credentials in urls are redacted from the logged server output.""" + await server.start() + await hass.async_block_till_done() + + assert_server_output_logged(expected_stdout, caplog, logging.WARNING) + assert "hunter2" not in caplog.text + assert "token" not in caplog.text + assert "alice@example.com" not in caplog.text + + await server.stop()