From be08912ed763f8b994556c8c624819095f5a4f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alptekin=20E=C4=9Fe?= Date: Sun, 2 Aug 2026 03:34:00 +0300 Subject: [PATCH] Fix redaction and sanitization gaps found in review redact(): the Authorization scrubber only handled the Bearer/Token schemes, so "Authorization: Basic " leaked the credential and Digest-style multi-token values leaked their parameters. Redact everything after the header name to the end of the line. sanitize(): the bidi-control blocklist missed the Cf format controls U+061C (Arabic Letter Mark) and U+08E2 (Arabic Disputed End of Ayah), both of which alter bidi processing when a hostile bio reaches the terminal. Add them to the zero-width/bidi set. Adds targeted regression tests for both; full suite 310 passed. --- githubfetch/api.py | 8 ++++++-- githubfetch/sanitize.py | 1 + tests/test_api.py | 23 +++++++++++++++++++++++ tests/test_sanitize.py | 7 +++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/githubfetch/api.py b/githubfetch/api.py index 91c70a8..1204ac3 100644 --- a/githubfetch/api.py +++ b/githubfetch/api.py @@ -59,9 +59,13 @@ def redact(text: object, token: str | None = None) -> str: if token: out = out.replace(token, "***REDACTED***") out = _TOKEN_PATTERN.sub("***REDACTED***", out) - # Never echo an Authorization header value back to the terminal. + # Never echo an Authorization header value back to the terminal. Redact + # everything after "Authorization:" to the end of the line - scheme and + # credential together, whatever the scheme (Bearer, Basic, Digest...), so + # a redaction cannot stop at the scheme name and leak the credential, nor + # leak multi-token values like Digest parameters. out = re.sub( - r"(?i)(authorization[\"']?\s*[:=]\s*[\"']?)(bearer|token)?\s*\S+", + r"(?i)(authorization[\"']?\s*[:=]\s*[\"']?)[^\n\r]*", r"\1***REDACTED***", out, ) diff --git a/githubfetch/sanitize.py b/githubfetch/sanitize.py index 6c85d6e..eda82c5 100644 --- a/githubfetch/sanitize.py +++ b/githubfetch/sanitize.py @@ -35,6 +35,7 @@ "\u202a\u202b\u202c\u202d\u202e" # LRE, RLE, PDF, LRO, RLO "\u2060\u2061\u2062\u2063\u2064" # word joiner + invisible operators "\u2066\u2067\u2068\u2069" # LRI, RLI, FSI, PDI + "\u061c\u08e2" # Arabic Letter Mark, Arabic Disputed End of Ayah "\ufeff" # BOM / ZWNBSP ) diff --git a/tests/test_api.py b/tests/test_api.py index 12055b4..9b4d2d9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -67,6 +67,29 @@ def test_redact_scrubs_authorization_header(): assert "abc123xyz" not in out +def test_redact_scrubs_basic_authorization_header(): + """Any auth scheme, not just Bearer/Token, must not leak its credential.""" + out = redact("Authorization: Basic dXNlcjpwYXNz") + assert "dXNlcjpwYXNz" not in out + assert "Basic" not in out + + +def test_redact_scrubs_arbitrary_auth_scheme(): + out = redact("authorization = \"Digest realm=example, nonce=deadbeef\"") + assert "nonce=deadbeef" not in out + assert "Digest" not in out + + +def test_redact_preserves_text_before_header_and_other_lines(): + out = redact("Error 401: Authorization: Bearer abc123 rejected") + assert "abc123" not in out + assert "Error 401:" in out + multiline = redact("GET /users HTTP/1.1\nAuthorization: Bearer abc123\nAccept: */*") + assert "abc123" not in multiline + assert "GET /users HTTP/1.1" in multiline + assert "Accept: */*" in multiline + + # ───────────────────────────── user fetch ─────────────────────────────────── @responses.activate def test_fetch_user_ok(user_payload): diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py index 70c9ba2..e68fc1d 100644 --- a/tests/test_sanitize.py +++ b/tests/test_sanitize.py @@ -46,6 +46,13 @@ def test_sanitize_strips_every_control_character(): assert sanitize_text(CONTROL_CHARS) == "" +def test_sanitize_strips_bidi_format_controls(): + """Cf bidi marks outside the common LRE/RLE set must also be dropped.""" + assert sanitize_text("evil\u061ctext") == "eviltext" # Arabic Letter Mark + assert sanitize_text("a\u08e2b") == "ab" # Arabic Disputed End of Ayah + assert "\u061c" not in sanitize_text("x" * 50 + "\u061c" * 50) + + def test_sanitize_preserves_normal_text(): assert sanitize_text("Hello, World! 123 — ok") == "Hello, World! 123 — ok"