Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions src/scp/sanitize_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{16,}={0,2}(?![A-Za-z0-9+/=])")
ENCODING_HEX = re.compile(r"\b(?=[0-9a-fA-F]*[a-fA-F])[0-9a-fA-F]{16,}\b")

_SCRIPT_LATIN = range(0x0041, 0x007B)
Expand Down Expand Up @@ -655,14 +655,22 @@ def _valid_base64_decode(blob: str) -> 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:
Expand Down
12 changes: 12 additions & 0 deletions tests/test_security_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading