From 6bab63fb0a11e087ef294518137c9a32f2506f46 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 15 Nov 2022 12:42:40 +0800 Subject: [PATCH] Redact more credentials in stream URL query params (#82089) Co-authored-by: Allen Porter --- homeassistant/components/stream/__init__.py | 21 ++++++++------- tests/components/stream/test_worker.py | 29 ++++++++++++++++----- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 7e6f663fc28..acc242006ca 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -20,7 +20,6 @@ import asyncio from collections.abc import Callable, Mapping import copy import logging -import re import secrets import threading import time @@ -28,6 +27,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING, Any, Final, cast import voluptuous as vol +from yarl import URL from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -91,17 +91,18 @@ __all__ = [ _LOGGER = logging.getLogger(__name__) -STREAM_SOURCE_REDACT_PATTERN = [ - (re.compile(r"//.*:.*@"), "//****:****@"), - (re.compile(r"\?auth=.*"), "?auth=****"), -] - -def redact_credentials(data: str) -> str: +def redact_credentials(url: str) -> str: """Redact credentials from string data.""" - for (pattern, repl) in STREAM_SOURCE_REDACT_PATTERN: - data = pattern.sub(repl, data) - return data + yurl = URL(url) + if yurl.user is not None: + yurl = yurl.with_user("****") + if yurl.password is not None: + yurl = yurl.with_password("****") + redacted_query_params = dict.fromkeys( + {"auth", "user", "password"} & yurl.query.keys(), "****" + ) + return str(yurl.update_query(redacted_query_params)) def create_stream( diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index e9f5769ddf1..a5a1f00d90a 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -727,11 +727,29 @@ async def test_update_stream_source(hass): await stream.stop() -async def test_worker_log(hass, caplog): +test_worker_log_cases = ( + ("https://abcd:efgh@foo.bar", "https://****:****@foo.bar"), + ( + "https://foo.bar/baz?user=abcd&password=efgh", + "https://foo.bar/baz?user=****&password=****", + ), + ( + "https://foo.bar/baz?param1=abcd¶m2=efgh", + "https://foo.bar/baz?param1=abcd¶m2=efgh", + ), + ( + "https://foo.bar/baz?param1=abcd&password=efgh", + "https://foo.bar/baz?param1=abcd&password=****", + ), +) + + +@pytest.mark.parametrize("stream_url, redacted_url", test_worker_log_cases) +async def test_worker_log(hass, caplog, stream_url, redacted_url): """Test that the worker logs the url without username and password.""" stream = Stream( hass, - "https://abcd:efgh@foo.bar", + stream_url, {}, hass.data[DOMAIN][ATTR_SETTINGS], dynamic_stream_settings(), @@ -740,13 +758,12 @@ async def test_worker_log(hass, caplog): with patch("av.open") as av_open, pytest.raises(StreamWorkerError) as err: av_open.side_effect = av.error.InvalidDataError(-2, "error") - run_worker(hass, stream, "https://abcd:efgh@foo.bar") + run_worker(hass, stream, stream_url) await hass.async_block_till_done() assert ( - str(err.value) - == "Error opening stream (ERRORTYPE_-2, error) https://****:****@foo.bar" + str(err.value) == f"Error opening stream (ERRORTYPE_-2, error) {redacted_url}" ) - assert "https://abcd:efgh@foo.bar" not in caplog.text + assert stream_url not in caplog.text @pytest.fixture