From 79f4a6230847301049631d792bbc4258af3afc93 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 15 Mar 2026 08:42:17 +0100 Subject: [PATCH] Redact printer access codes from support bundle logs RTSP stream URLs (rtsps://bblp:@:322/...) were not covered by the credential sanitizer, leaking access codes in support bundles and bug report logs. Extended the URL regex to match rtsps:// and added access codes to the sensitive string collection for exact-match redaction in both export paths. --- CHANGELOG.md | 1 + backend/app/api/routes/support.py | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0c1fa3ff..7c34f420b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ All notable changes to Bambuddy will be documented in this file. ### Security - **PyJWT ≥2.12.0** — Bumped minimum version to address CVE-2026-32597. - **flatted ≥3.4.0** — Updated transitive ESLint dependency to address GHSA-25h7-pfq9-p65f (unbounded recursion DoS). +- **Access Code Redacted from Support Logs** — Printer access codes embedded in RTSP stream URLs were not redacted in support bundles and bug report logs. Extended the URL credential sanitizer to cover `rtsps://` URLs and added access codes to the sensitive string collection for exact-match redaction. ### Changed - **CI: Node.js 20 → 22** — Updated GitHub Actions workflows (`ci.yml`, `security.yml`) from Node.js 20 to Node.js 22 LTS ahead of [GitHub's Node 20 deprecation](https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/). diff --git a/backend/app/api/routes/support.py b/backend/app/api/routes/support.py index 9d7682e06..bc093a88a 100644 --- a/backend/app/api/routes/support.py +++ b/backend/app/api/routes/support.py @@ -735,8 +735,8 @@ def _sanitize_log_content(content: str, sensitive_strings: dict[str, str] | None continue # Skip very short strings to prevent over-redaction content = re.sub(re.escape(value), label, content) - # Replace credentials in URLs (e.g. http://user:pass@host) - content = re.sub(r"(https?://)[^/:@\s]+:[^/@\s]+@", r"\1[CREDENTIALS]@", content) + # Replace credentials in URLs (e.g. http://user:pass@host, rtsps://bblp:code@host) + content = re.sub(r"((?:https?|rtsps?)://)[^/:@\s]+:[^/@\s]+@", r"\1[CREDENTIALS]@", content) # Replace email addresses content = re.sub(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", content) @@ -786,14 +786,16 @@ async def _get_recent_sanitized_logs(max_lines: int = 200) -> str: # Collect sensitive strings from DB for redaction sensitive_strings: dict[str, str] = {} async with async_session() as db: - result = await db.execute(select(Printer.name, Printer.serial_number, Printer.ip_address)) - for name, serial, ip_address in result.all(): + result = await db.execute(select(Printer.name, Printer.serial_number, Printer.ip_address, Printer.access_code)) + for name, serial, ip_address, access_code in result.all(): if name: sensitive_strings[name] = "[PRINTER]" if serial: sensitive_strings[serial] = "[SERIAL]" if ip_address: sensitive_strings[ip_address] = "[IP]" + if access_code: + sensitive_strings[access_code] = "[ACCESS_CODE]" result = await db.execute(select(User.username)) for (username,) in result.all(): @@ -839,15 +841,17 @@ async def generate_support_bundle( # Collect known sensitive values for log redaction sensitive_strings: dict[str, str] = {} - # Printer names, serial numbers, and IP addresses - result = await db.execute(select(Printer.name, Printer.serial_number, Printer.ip_address)) - for name, serial, ip_address in result.all(): + # Printer names, serial numbers, IP addresses, and access codes + result = await db.execute(select(Printer.name, Printer.serial_number, Printer.ip_address, Printer.access_code)) + for name, serial, ip_address, access_code in result.all(): if name: sensitive_strings[name] = "[PRINTER]" if serial: sensitive_strings[serial] = "[SERIAL]" if ip_address: sensitive_strings[ip_address] = "[IP]" + if access_code: + sensitive_strings[access_code] = "[ACCESS_CODE]" # Auth usernames result = await db.execute(select(User.username))