fix: keep server paths and URLs out of an error response and a debug log (#1558)

Two small leaks from going through the open CodeQL alerts (code scanning
alerts 100 and 123).

The local download route returned `str(e)` on an OSError, and for a
missing or unreadable file that text is the file's path on the server
(`[Errno 2] No such file or directory:
'/audiobooks/Author/Title.epub'`). #1422 cuts a non-admin's download
path down to the file name everywhere else, so this put it back. The
route now answers "Could not read the file" and the detail stays in the
log.

`should_bypass_proxy` logged the whole request URL at debug when it
could not be parsed. URLs can carry credentials, so it logs only the
exception now.

Each change has a test that fails without it. The full non-e2e suite,
ruff and basedpyright pass.
This commit is contained in:
splitsec2 authored and GitHub committed 2026-10-23 01:52:39 -06:00
1 parent 003df0b686
commit 4f97fc9bec
4 files changed
+58 -2

No files matched your search

+1 -1
View File
@@ -53,7 +53,7 @@ def should_bypass_proxy(url: str) -> bool:
parsed = urllib.parse.urlparse(url)
hostname = (parsed.hostname or "").lower()
except ValueError as e:
logger.debug("Failed to parse URL for proxy bypass check: %s - %s", url, e)
logger.debug("Failed to parse URL for proxy bypass check: %s", e)
return False
if not hostname:
+3 -1
View File
@@ -2022,8 +2022,10 @@ def api_local_download() -> Response | tuple[Response, int]:
return send_file(file_paths[0], download_name=file_name, as_attachment=True)
except _OPERATIONAL_ERRORS as e:
# The details are logged. The message of an OSError names the file's path on the server,
# which a non-admin never sees anywhere else.
logger.error_trace(f"Local download error: {e}")
return jsonify({"error": str(e)}), 500
return jsonify({"error": "Could not read the file"}), 500
@app.route("/api/covers/<cover_id>", methods=["GET"])
+38
View File
@@ -357,6 +357,44 @@ class TestActivityRoutes:
assert response.status_code == 200
assert response.data == file_bytes
def test_localdownload_error_does_not_reveal_the_server_path(
self, main_module, client, tmp_path
):
"""A failed read answers with a fixed message, not the OSError text and its path."""
user = _create_user(main_module, prefix="reader")
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
task_id = f"live-localdownload-{uuid.uuid4().hex[:8]}"
file_path = tmp_path / "Hidden Author" / "Hidden Title.epub"
file_path.parent.mkdir()
file_path.write_bytes(b"payload")
task = DownloadTask(
task_id=task_id,
source="direct_download",
title="Live Local Download",
user_id=user["id"],
username=user["username"],
download_path=str(file_path),
)
assert main_module.backend.book_queue.add(task) is True
failure = FileNotFoundError(2, "No such file or directory", str(file_path))
try:
main_module.backend.book_queue.update_status(task_id, QueueStatus.COMPLETE)
with (
patch.object(main_module, "get_auth_mode", return_value="builtin"),
patch.object(main_module, "send_file", side_effect=failure),
):
response = client.get(f"/api/localdownload?id={task_id}")
finally:
main_module.backend.book_queue.cancel_download(task_id)
assert response.status_code == 500
assert response.get_json() == {"error": "Could not read the file"}
assert "Hidden" not in response.get_data(as_text=True)
assert str(tmp_path) not in response.get_data(as_text=True)
def test_localdownload_zips_a_live_multi_file_download(self, main_module, client, tmp_path):
user = _create_user(main_module, prefix="reader")
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
@@ -59,3 +59,19 @@ def test_get_proxies_returns_socks_proxy_for_both_schemes(monkeypatch):
"http": "socks5://proxy.local:1080",
"https": "socks5://proxy.local:1080",
}
def test_a_url_that_fails_to_parse_is_not_written_to_the_log(monkeypatch):
"""The proxy check logs why a URL did not parse, never the URL itself."""
from unittest.mock import patch
network = _set_proxy_config(monkeypatch, NO_PROXY="localhost")
# An unterminated IPv6 literal makes urlparse raise ValueError.
url = "http://user:s3cret@[::1/path?apikey=s3cret"
with patch.object(network.logger, "debug") as debug:
assert network.should_bypass_proxy(url) is False
logged = " ".join(str(arg) for call in debug.call_args_list for arg in call.args)
assert "Failed to parse URL" in logged
assert "s3cret" not in logged