Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 16 additions & 3 deletions repo/plugin.video.nzbdav/resources/lib/http_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@
re.IGNORECASE,
)

# Some Newznab indexers return download URLs shaped like
# ``/getnzb/id.nzb&i=ACCOUNT&r=APIKEY``. Because there is no ``?``, both
# credentials are parsed as part of the URL path rather than query params.
# Restrict the short ``i``/``r`` names to getnzb-looking paths so ordinary
# application URLs using those names are not over-redacted.
_GETNZB_PATH_CRED_RE = re.compile(
r"([&;](?:i|r))=([^&\s\"'<>]+)",
re.IGNORECASE,
)

# Catch ``scheme://user:password@host`` userinfo embedded in free-form text.
# urllib / socket / xbmcvfs errors sometimes echo the failing URL — e.g. the
# NZBGet JSON-RPC URL or the ``smb://user:pass@host/...`` completed-folder
Expand Down Expand Up @@ -91,9 +101,10 @@ def redact_url(url):
# WebDAV stack used to accept). Strip the password half before
# logging. TODO.md §H.2-H2d.
netloc = _redact_netloc_userinfo(parts.netloc)
return urlunsplit(
(parts.scheme, netloc, parts.path, urlencode(query), parts.fragment)
)
path = parts.path
if "getnzb" in path.lower() or ".nzb&" in path.lower():
path = _GETNZB_PATH_CRED_RE.sub(r"\1=REDACTED", path)
return urlunsplit((parts.scheme, netloc, path, urlencode(query), parts.fragment))


def _redact_netloc_userinfo(netloc):
Expand Down Expand Up @@ -146,6 +157,8 @@ def redact_text(text):
# ``\1`` is the key group; a backreference replacement avoids a per-match
# Python callback on this hot logging/error path.
redacted = _EMBEDDED_CRED_RE.sub(r"\1=REDACTED", str(text))
if "getnzb" in redacted.lower() or ".nzb&" in redacted.lower():
redacted = _GETNZB_PATH_CRED_RE.sub(r"\1=REDACTED", redacted)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scope GETNzb redaction to URL paths.

Unlike redact_url(), this applies _GETNZB_PATH_CRED_RE to the entire free-form string once any getnzb marker is present. It therefore redacts unrelated &i=/&r= parameters in another URL, ordinary text, or even the query portion of the same URL. Apply the substitution only to matched URL paths and add a mixed-message regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@repo/plugin.video.nzbdav/resources/lib/http_util.py` around lines 160 - 161,
Update the GETNzb handling in redact_url() so _GETNZB_PATH_CRED_RE is applied
only to matched URL path components, not the entire free-form redacted string.
Preserve unrelated URLs, ordinary text, and query parameters, and add a
regression test covering a message containing both a GETNzb URL and unrelated
content.

return _EMBEDDED_URL_RE.sub(_redact_url_userinfo_span, redacted)


Expand Down
32 changes: 32 additions & 0 deletions tests/test_http_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,24 @@ def test_redact_url_preserves_url_without_apikey():
assert result == url


def test_redact_url_hides_getnzb_path_credentials():
"""Newznab links may put account/API credentials in the path."""
url = "https://indexer.example/getnzb/id.nzb&i=12345&r=secretkey123"
result = redact_url(url)

assert "12345" not in result
assert "secretkey123" not in result
assert "&i=REDACTED" in result
assert "&r=REDACTED" in result


def test_redact_url_preserves_short_params_outside_getnzb_paths():
"""Short i/r parameters are not credentials in arbitrary URLs."""
url = "https://example.com/report&i=12345&r=summary"

assert redact_url(url) == url


def test_redact_url_hides_extended_credential_keys():
"""TODO.md §H.2-H2c: the redaction set covers more than just apikey.
`key`, `access_token`, `bearer`, `session`, `sessionid`, `password`,
Expand Down Expand Up @@ -285,6 +303,20 @@ def test_redact_text_redacts_multiple_credential_params():
assert "t=movie" in result


def test_redact_text_hides_getnzb_path_credentials():
"""Free-form HTTP errors must scrub non-standard Newznab path secrets."""
msg = (
"fetch failed for "
"https://indexer.example/getnzb/id.nzb&i=12345&r=secretkey123"
)
result = redact_text(msg)

assert "12345" not in result
assert "secretkey123" not in result
assert "&i=REDACTED" in result
assert "&r=REDACTED" in result


def test_redact_text_redacts_digit_prefixed_values():
"""The ``\\1=REDACTED`` backreference must not misfire when the secret value
begins with a digit — the literal ``=`` terminates the group, so there is no
Expand Down