diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a58c15..7702146 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/src/vudials_client/vudialsclient.py b/src/vudials_client/vudialsclient.py index 35891ab..afa38f9 100644 --- a/src/vudials_client/vudialsclient.py +++ b/src/vudials_client/vudialsclient.py @@ -13,7 +13,7 @@ def get_uri(self, server_url: str, api_key: str, api_call: str, keyword_params: # This means it will appear in server access logs, proxy logs, and # HTTP client history. If the server adds header-based authentication # in the future, prefer an Authorization or X-API-Key header instead. - return f'{server_url}/api/v0/{api_call}?key={api_key}{keyword_params}' + return f'{server_url}/api/v0/{api_call}?key={quote(api_key)}{keyword_params}' def send_http_request(self, path_uri: str, files: dict, timeout: int = 10) -> requests.Response: if files is not None: @@ -27,7 +27,7 @@ def send_http_request(self, path_uri: str, files: dict, timeout: int = 10) -> re class VUAdminUtil: def get_uri(self, server_url: str, api_key: str, api_call: str, keyword_params: str) -> str: # Security note: See VUUtil.get_uri — same key-in-URL caveat applies. - return f'{server_url}/api/v0/{api_call}?admin_key={api_key}{keyword_params}' + return f'{server_url}/api/v0/{api_call}?admin_key={quote(api_key)}{keyword_params}' def send_http_request(self, path_uri: str, method: str, timeout: int = 10) -> requests.Response: if method == "post": diff --git a/tests/test_vudialsclient.py b/tests/test_vudialsclient.py index 817a170..2887c4d 100644 --- a/tests/test_vudialsclient.py +++ b/tests/test_vudialsclient.py @@ -49,6 +49,12 @@ def test_multiple_keyword_params(self): assert "green=128" in uri assert "blue=0" in uri + def test_key_with_special_chars_url_encoded(self): + # A key containing & and = must not break the query string structure. + uri = self.util.get_uri("http://localhost:5340", "k&admin_key=evil", "dial/list", "") + assert "k%26admin_key%3Devil" in uri + assert "admin_key=evil" not in uri + class TestVUUtilSendHttpRequest: def setup_method(self): @@ -138,6 +144,12 @@ def test_api_path_format(self): uri = self.util.get_uri("http://192.168.0.1:9000", "k", "admin/keys/list", "") assert uri.startswith("http://192.168.0.1:9000/api/v0/") + def test_admin_key_with_special_chars_url_encoded(self): + # A key containing & and = must not inject extra query parameters. + uri = self.util.get_uri("http://localhost:5340", "a&key=evil", "admin/keys/list", "") + assert "a%26key%3Devil" in uri + assert "key=evil" not in uri + class TestVUAdminUtilSendHttpRequest: def setup_method(self):