diff --git a/frigate/const.py b/frigate/const.py index 3b118682a1..b4da7e8f8a 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -117,10 +117,16 @@ SUPPORTED_RK_SOCS = ["rk3562", "rk3566", "rk3568", "rk3576", "rk3588"] REGEX_CAMERA_NAME = r"^[a-zA-Z0-9_-]+$" REGEX_RTSP_CAMERA_USER_PASS = r":\/\/[a-zA-Z0-9_-]+:[\S]+@" -REGEX_URL_USERINFO = r"://(?:[^\s/:]*:\S*|[^\s/]+)@" +# A "user:" userinfo may hold "/", "#" or "@" in the password, so it runs to the +# last "@" before the next URL. A port after the colon is rejected so a bare +# host:port/path@x is not taken for credentials. Anything else stops at the +# authority end. +REGEX_URL_USERINFO = ( + r"://(?:[^\s/?#:\[\]]*:(?!\d+(?:[/?#,\s]|$))(?:(?!://)\S)*|[^\s/?#]+)@" +) _CREDENTIAL_QUERY_KEYS = ( r"(?:user(?:name)?|usr|login|pass(?:word|wd)?|pwd" - r"|[a-z_]*token|[a-z_]*secret|[a-z_]*private|api_?key|auth)" + r"|(?:[a-z_]*_)?token|(?:[a-z_]*_)?secret|client_private|api_?key)" ) REGEX_CREDENTIAL_QUERY_PARAM = rf"(?i)([?&]{_CREDENTIAL_QUERY_KEYS}=)[^&\s]*" REGEX_MASKED_CREDENTIAL = rf"(?i)://\*:\*@|[?&]{_CREDENTIAL_QUERY_KEYS}=\*(?![^&\s])" diff --git a/frigate/test/test_camera_pw.py b/frigate/test/test_camera_pw.py index 0a058b9269..09c86bfffc 100644 --- a/frigate/test/test_camera_pw.py +++ b/frigate/test/test_camera_pw.py @@ -1,5 +1,6 @@ """Test camera user and password cleanup.""" +import time import unittest from frigate.util.builtin import ( @@ -57,6 +58,11 @@ class TestUserPassMasking(unittest.TestCase): "rtsp://:pw@10.0.0.2/live": "rtsp://*:*@10.0.0.2/live", "tapo://cloudpassword@10.0.0.2": "tapo://*:*@10.0.0.2", "rtsp://u:p@ss@10.0.0.2/live": "rtsp://*:*@10.0.0.2/live", + "rtsp://user:pa/ss@10.0.0.2/live": "rtsp://*:*@10.0.0.2/live", + "rtsp://u:p@ss#x@10.0.0.2/live": "rtsp://*:*@10.0.0.2/live", + "http://h/x?api_key=a&apikey=b&access_token=c": ( + "http://h/x?api_key=*&apikey=*&access_token=*" + ), "http://h/cgi?user=admin&password=x": "http://h/cgi?user=*&password=*", "http://h/cgi?password=x&user=admin&channel=1": ( "http://h/cgi?password=*&user=*&channel=1" @@ -94,6 +100,11 @@ class TestUserPassMasking(unittest.TestCase): "ffmpeg:front_door#video=copy", "rtsp://127.0.0.1:8554/front_door?video=copy&audio=aac", "homekit://10.0.0.2:51827?device_id=d", + "rtsp://10.0.0.1:554/live?x=a@b", + "rtsp://10.0.0.1:554/path@thing", + "http://10.0.0.1/snap.jpg?email=me@x.com", + "http://h/x?auth=a&author=b&authorization=c&user_agent=d&autoken=e", + "http://h/x?private=1&passthrough=1", "Camera front_door is not responding", ): with self.subTest(line=line): @@ -102,5 +113,17 @@ class TestUserPassMasking(unittest.TestCase): def test_commas_outside_credentials_survive(self): """ffprobe splits multiple paths on commas left after masking.""" - self.assertIn(",", clean_camera_user_pass("rtsp://a/x,rtsp://u:p@b/y")) + for line in ( + "rtsp://a/x,rtsp://u:p@b/y", + "rtsp://a:554/x,rtsp://u:p@b/y", + ): + with self.subTest(line=line): + self.assertIn(",", clean_camera_user_pass(line)) self.assertNotIn(",", clean_camera_user_pass("rtsp://u:p,q@b/y")) + + def test_long_line_masks_in_linear_time(self): + """Log lines run through the mask, so a long one must stay fast.""" + start = time.perf_counter() + clean_camera_user_pass("rtsp://u:p" * 20000) + clean_camera_user_pass("rtsp://" + "a." * 40000) + self.assertLess(time.perf_counter() - start, 1) diff --git a/web/src/utils/credentialMask.ts b/web/src/utils/credentialMask.ts index 56eb390437..2438475be8 100644 --- a/web/src/utils/credentialMask.ts +++ b/web/src/utils/credentialMask.ts @@ -1,7 +1,8 @@ // Mirrors clean_camera_user_pass and REGEX_MASKED_CREDENTIAL in the backend. const CREDENTIAL_QUERY_KEYS = - "(?:user(?:name)?|usr|login|pass(?:word|wd)?|pwd|[a-z_]*token|[a-z_]*secret|[a-z_]*private|api_?key|auth)"; -const URL_USERINFO_PATTERN = /:\/\/(?:[^\s/:]*:\S*|[^\s/]+)@/g; + "(?:user(?:name)?|usr|login|pass(?:word|wd)?|pwd|(?:[a-z_]*_)?token|(?:[a-z_]*_)?secret|client_private|api_?key)"; +const URL_USERINFO_PATTERN = + /:\/\/(?:[^\s/?#:[\]]*:(?!\d+(?:[/?#,\s]|$))(?:(?!:\/\/)\S)*|[^\s/?#]+)@/g; const CREDENTIAL_QUERY_PATTERN = new RegExp( `([?&]${CREDENTIAL_QUERY_KEYS}=)[^&\\s]*`, "gi",