diff --git a/src/scp/sanitize_input.py b/src/scp/sanitize_input.py index 3084b46..6caf0ee 100644 --- a/src/scp/sanitize_input.py +++ b/src/scp/sanitize_input.py @@ -406,7 +406,7 @@ def _prepare_text_for_scan(text: str) -> str: ] MORSE_PATTERN = re.compile(r"[.-]{3,}") -ENCODING_BASE64 = re.compile(r"(?=[A-Za-z0-9+/]*[+/=])[A-Za-z0-9+/]{16,}={0,2}") +ENCODING_BASE64 = re.compile(r"(? bool: return bool(text) and sum(1 for c in text if c.isprintable() or c in "\n\r\t") >= len(text) * 0.9 +def _has_base64_marker(blob: str) -> bool: + return "+" in blob or "/" in blob or blob.endswith("=") + + def scan_encoding_blocks(text: str) -> list[tuple[int, str]]: findings = [] - for pat in (ENCODING_BASE64, ENCODING_HEX): - for m in pat.finditer(text): - matched = m.group(0) - if pat == ENCODING_BASE64 and (_looks_like_path(matched) or _looks_like_identifier_or_constant(matched)): - continue - findings.append((m.start(), matched[:50] + ("..." if len(matched) > 50 else ""))) + for m in ENCODING_BASE64.finditer(text): + matched = m.group(0) + if not _has_base64_marker(matched): + continue + if _looks_like_path(matched) or _looks_like_identifier_or_constant(matched): + continue + findings.append((m.start(), matched[:50] + ("..." if len(matched) > 50 else ""))) + for m in ENCODING_HEX.finditer(text): + matched = m.group(0) + findings.append((m.start(), matched[:50] + ("..." if len(matched) > 50 else ""))) for m in _B64_CANDIDATE.finditer(text): matched = m.group(0).rstrip("=") if len(matched) < 24: diff --git a/tests/test_security_regressions.py b/tests/test_security_regressions.py index 0d9f23e..3b7bfbd 100644 --- a/tests/test_security_regressions.py +++ b/tests/test_security_regressions.py @@ -44,6 +44,18 @@ def test_classify_many_backticks_completes_quickly() -> None: assert time.perf_counter() - t0 < 5.0 +def test_scan_encoding_blocks_long_alpha_run_completes_quickly() -> None: + s = "Z" * 50_000 + t0 = time.perf_counter() + sanitize_input.scan_encoding_blocks(s) + assert time.perf_counter() - t0 < 0.5 + + +def test_scan_encoding_blocks_still_flags_padded_base64() -> None: + findings = sanitize_input.scan_encoding_blocks("prefix QUJDREVGR0hJSktMTU5P== suffix") + assert any("QUJDREVGR0hJSktMTU5P" in phrase for _, phrase in findings) + + def test_mask_long_almost_email_completes_quickly() -> None: s = ("a" * 100_000) + "@example.com" t0 = time.perf_counter()