Skip to content

Commit 2986a58

Browse files
committed
security: harden encoder log redaction
1 parent 869a947 commit 2986a58

4 files changed

Lines changed: 35 additions & 36 deletions

File tree

bin/youtube-autoencoder

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ from __future__ import annotations
77
import json
88
import os
99
import pathlib
10+
import re
1011
import selectors
1112
import signal
1213
import subprocess
@@ -30,6 +31,9 @@ OBS_SCENE_FILE = pathlib.Path(
3031

3132
child: subprocess.Popen[Any] | None = None
3233
stopping = False
34+
URL_CREDENTIALS_RE = re.compile(r"(?P<scheme>[A-Za-z][A-Za-z0-9+.-]*://)[^/\s:@]+(?::[^/\s@]*)?@")
35+
RTMP_SECRET_PATH_RE = re.compile(r"(?i)\b(?P<prefix>rtmps?://[^\s/]+/)[^\s]+")
36+
YOUTUBE_SECRET_PATH_RE = re.compile(r"(?i)\b(?P<prefix>https?://[^\s/]*(?:youtube|google)[^\s/]*/)[^\s]+")
3337

3438

3539
def log(message: str) -> None:
@@ -71,22 +75,10 @@ def redact_url(url: str) -> str:
7175
return f"{parts.scheme}://{host}{path}"
7276

7377

74-
def redact_text(text: str, secrets: list[str]) -> str:
75-
redacted = text
76-
for secret in secrets:
77-
if not secret:
78-
continue
79-
redacted = redacted.replace(secret, redact_url(secret))
80-
parts = urlsplit(secret)
81-
if parts.password:
82-
redacted = redacted.replace(parts.password, "<redacted>")
83-
if parts.username:
84-
redacted = redacted.replace(parts.username, "<redacted>")
85-
if parts.path and ("youtube" in (parts.hostname or "") or "rtmp" in parts.scheme):
86-
stream_key = parts.path.rsplit("/", 1)[-1]
87-
if stream_key:
88-
redacted = redacted.replace(stream_key, "<redacted>")
89-
return redacted
78+
def redact_text(text: str) -> str:
79+
redacted = URL_CREDENTIALS_RE.sub(r"\g<scheme><redacted>@", text)
80+
redacted = RTMP_SECRET_PATH_RE.sub(r"\g<prefix><redacted>", redacted)
81+
return YOUTUBE_SECRET_PATH_RE.sub(r"\g<prefix><redacted>", redacted)
9082

9183

9284
def load_json(path: pathlib.Path) -> dict[str, Any]:
@@ -172,11 +164,11 @@ def source_available(url: str) -> bool:
172164
check=False,
173165
)
174166
except subprocess.TimeoutExpired:
175-
log(f"source probe timed out source={redact_url(url)}")
167+
log("source probe timed out")
176168
return False
177169
if result.returncode != 0:
178-
details = redact_text((result.stderr or result.stdout).strip(), [url])
179-
log(f"source probe failed: {details}" if details else f"source probe failed source={redact_url(url)}")
170+
details = redact_text((result.stderr or result.stdout).strip())
171+
log(f"source probe failed: {details}" if details else "source probe failed")
180172
return False
181173
return True
182174

@@ -334,7 +326,7 @@ def run_once() -> int:
334326
raise RuntimeError("prepare-broadcast returned incomplete state")
335327
input_url, ingest_url = stream_config()
336328

337-
log(f"starting ffmpeg mode={env('YTA_MODE', 'copy')} source={redact_url(input_url)} target={redact_url(ingest_url)}")
329+
log(f"starting ffmpeg mode={env('YTA_MODE', 'copy')}")
338330
child = subprocess.Popen(
339331
ffmpeg_args(input_url, ingest_url),
340332
stderr=subprocess.STDOUT,
@@ -355,7 +347,7 @@ def run_once() -> int:
355347
for key, _mask in selector.select(timeout=1):
356348
line = key.fileobj.readline()
357349
if line:
358-
log("ffmpeg: " + redact_text(line.rstrip(), [input_url, ingest_url]))
350+
log("ffmpeg: " + redact_text(line.rstrip()))
359351
if stopping:
360352
break
361353
if max_runtime and time.monotonic() - started > max_runtime:
@@ -366,7 +358,7 @@ def run_once() -> int:
366358
rc = child.wait(timeout=20)
367359
if child.stdout is not None:
368360
for line in child.stdout:
369-
log("ffmpeg: " + redact_text(line.rstrip(), [input_url, ingest_url]))
361+
log("ffmpeg: " + redact_text(line.rstrip()))
370362
return rc
371363
except subprocess.TimeoutExpired:
372364
log("ffmpeg did not stop cleanly; killing")

bin/youtube-autoencoder-test-pattern

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ from __future__ import annotations
77
import json
88
import os
99
import pathlib
10+
import re
1011
import selectors
1112
import signal
1213
import subprocess
@@ -24,6 +25,8 @@ OBS_SERVICE_FILE = pathlib.Path(os.environ.get("YTA_OBS_SERVICE_FILE", OBS_PROFI
2425

2526
child: subprocess.Popen[Any] | None = None
2627
stopping = False
28+
URL_CREDENTIALS_RE = re.compile(r"(?P<scheme>[A-Za-z][A-Za-z0-9+.-]*://)[^/\s:@]+(?::[^/\s@]*)?@")
29+
RTMP_SECRET_PATH_RE = re.compile(r"(?i)\b(?P<prefix>rtmps?://[^\s/]+/)[^\s]+")
2730

2831

2932
def log(message: str) -> None:
@@ -39,16 +42,9 @@ def redact_url(url: str) -> str:
3942
return f"{parts.scheme}://{host}{path}"
4043

4144

42-
def redact_text(text: str, secrets: list[str]) -> str:
43-
redacted = text
44-
for secret in secrets:
45-
if not secret:
46-
continue
47-
redacted = redacted.replace(secret, redact_url(secret))
48-
stream_key = urlsplit(secret).path.rsplit("/", 1)[-1]
49-
if stream_key:
50-
redacted = redacted.replace(stream_key, "<redacted>")
51-
return redacted
45+
def redact_text(text: str) -> str:
46+
redacted = URL_CREDENTIALS_RE.sub(r"\g<scheme><redacted>@", text)
47+
return RTMP_SECRET_PATH_RE.sub(r"\g<prefix><redacted>", redacted)
5248

5349

5450
def youtube_url() -> str:
@@ -126,7 +122,7 @@ def ffmpeg_args(output_url: str) -> list[str]:
126122
def run(duration: int) -> int:
127123
global child
128124
output_url = youtube_url()
129-
log(f"starting test pattern duration={duration}s target={redact_url(output_url)}")
125+
log(f"starting test pattern duration={duration}s")
130126
child = subprocess.Popen(
131127
ffmpeg_args(output_url),
132128
stdout=subprocess.PIPE,
@@ -143,7 +139,7 @@ def run(duration: int) -> int:
143139
for key, _mask in selector.select(timeout=1):
144140
line = key.fileobj.readline()
145141
if line:
146-
log("ffmpeg: " + redact_text(line.rstrip(), [output_url]))
142+
log("ffmpeg: " + redact_text(line.rstrip()))
147143
if time.monotonic() >= deadline:
148144
log("test duration reached; stopping ffmpeg")
149145
child.send_signal(signal.SIGINT)
@@ -156,7 +152,7 @@ def run(duration: int) -> int:
156152
rc = child.wait(timeout=10)
157153
if child.stdout is not None:
158154
for line in child.stdout:
159-
log("ffmpeg: " + redact_text(line.rstrip(), [output_url]))
155+
log("ffmpeg: " + redact_text(line.rstrip()))
160156
log(f"test pattern exited rc={rc}")
161157
return 0 if rc in (0, 255) else rc
162158
finally:

tests/test_supervisor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ def test_redacts_credentials_and_stream_keys(load_script):
1919
assert supervisor.redact_url(camera_url) == "rtsp://<redacted>@camera.local:554/stream1"
2020
assert supervisor.redact_url(ingest_url) == "rtmps://a.rtmp.youtube.com/<redacted>"
2121

22-
redacted = supervisor.redact_text(f"input={camera_url} output={ingest_url}", [camera_url, ingest_url])
22+
redacted = supervisor.redact_text(f"input={camera_url} output={ingest_url}")
2323
assert "user" not in redacted
2424
assert "pass" not in redacted
2525
assert "secret-stream-key" not in redacted
26+
assert "rtsp://<redacted>@camera.local:554/stream1" in redacted
27+
assert "rtmps://a.rtmp.youtube.com/<redacted>" in redacted
2628

2729

2830
def test_selected_vlc_url_prefers_named_selected_source(load_script, monkeypatch):

tests/test_test_pattern.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,12 @@ def test_test_pattern_ffmpeg_args(load_script):
1212
assert "sine=frequency=440:sample_rate=48000" in args
1313
assert args[args.index("-c:v") + 1] == "libx264"
1414
assert args[-1] == output_url
15+
16+
17+
def test_test_pattern_redacts_rtmp_secret_from_log_lines(load_script):
18+
test_pattern = load_script("youtube-autoencoder-test-pattern", "yta_test_pattern_redaction")
19+
20+
redacted = test_pattern.redact_text("target=rtmps://youtube.example/live/secret-stream-key")
21+
22+
assert redacted == "target=rtmps://youtube.example/<redacted>"
23+
assert "secret-stream-key" not in redacted

0 commit comments

Comments
 (0)