Browse Source

Redact printer access codes from support bundle logs

  RTSP stream URLs (rtsps://bblp:<code>@<ip>: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.
maziggy 2 months ago
parent
commit
79f4a62308
2 changed files with 12 additions and 7 deletions
  1. 1 0
      CHANGELOG.md
  2. 11 7
      backend/app/api/routes/support.py

+ 1 - 0
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/).

+ 11 - 7
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))