Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/vudials_client/vudialsclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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":
Expand Down
12 changes: 12 additions & 0 deletions tests/test_vudialsclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down