fix: require HTTPS for Redmine URL
Config.validate() now rejects REDMINE_URL values whose scheme is not HTTPS (http://, missing scheme, or any other scheme). The API key is sent in request headers, so a plain-HTTP endpoint would expose it. Scheme comparison is case-insensitive per RFC 3986. Closes #54
This commit is contained in:
@@ -317,3 +317,4 @@ mypy redmine_reporter
|
|||||||
- Рекомендуется хранить секреты через `${VAR}`, а не plaintext.
|
- Рекомендуется хранить секреты через `${VAR}`, а не plaintext.
|
||||||
- Используйте аккаунт с минимальными правами, достаточными для чтения time entries и задач.
|
- Используйте аккаунт с минимальными правами, достаточными для чтения time entries и задач.
|
||||||
- Инструмент работает только в режиме чтения и не изменяет данные в Redmine.
|
- Инструмент работает только в режиме чтения и не изменяет данные в Redmine.
|
||||||
|
- `REDMINE_URL` обязан использовать HTTPS: API-ключ передаётся в заголовках запроса, и без TLS он может быть перехвачен.
|
||||||
|
|||||||
@@ -398,8 +398,14 @@ class Config:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls) -> None:
|
def validate(cls) -> None:
|
||||||
if not cls.get_redmine_url():
|
url = cls.get_redmine_url()
|
||||||
|
if not url:
|
||||||
raise ValueError("REDMINE_URL is required (set via env or .env)")
|
raise ValueError("REDMINE_URL is required (set via env or .env)")
|
||||||
|
if not url.lower().startswith("https://"):
|
||||||
|
raise ValueError(
|
||||||
|
"REDMINE_URL must use HTTPS: the API key is sent in request "
|
||||||
|
"headers and requires TLS"
|
||||||
|
)
|
||||||
if cls.get_redmine_api_key():
|
if cls.get_redmine_api_key():
|
||||||
return
|
return
|
||||||
if not (cls.get_redmine_user() and cls.get_redmine_password()):
|
if not (cls.get_redmine_user() and cls.get_redmine_password()):
|
||||||
|
|||||||
@@ -126,6 +126,56 @@ def test_get_redmine_verify_custom_path():
|
|||||||
assert Config.get_redmine_verify() == "/tmp/redmine-ca.pem"
|
assert Config.get_redmine_verify() == "/tmp/redmine-ca.pem"
|
||||||
|
|
||||||
|
|
||||||
|
# -- #54: REDMINE_URL обязан использовать HTTPS --
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfigRequiresHttpsUrl:
|
||||||
|
"""Config.validate() отклоняет URL без TLS: API-ключ идёт в заголовках."""
|
||||||
|
|
||||||
|
@mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"REDMINE_URL": "http://red.eltex.loc/", "REDMINE_API_KEY": "token"},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
def test_http_url_rejected(self):
|
||||||
|
with pytest.raises(ValueError, match="HTTPS"):
|
||||||
|
Config.validate()
|
||||||
|
|
||||||
|
@mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"REDMINE_URL": "red.eltex.loc", "REDMINE_API_KEY": "token"},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
def test_url_without_scheme_rejected(self):
|
||||||
|
with pytest.raises(ValueError, match="HTTPS"):
|
||||||
|
Config.validate()
|
||||||
|
|
||||||
|
@mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"REDMINE_URL": "ftp://red.eltex.loc/", "REDMINE_API_KEY": "token"},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
def test_non_http_scheme_rejected(self):
|
||||||
|
with pytest.raises(ValueError, match="HTTPS"):
|
||||||
|
Config.validate()
|
||||||
|
|
||||||
|
@mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"REDMINE_URL": "https://red.eltex.loc/", "REDMINE_API_KEY": "token"},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
def test_https_url_accepted(self):
|
||||||
|
Config.validate()
|
||||||
|
|
||||||
|
@mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"REDMINE_URL": "HTTPS://red.eltex.loc/", "REDMINE_API_KEY": "token"},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
def test_uppercase_scheme_accepted(self):
|
||||||
|
Config.validate()
|
||||||
|
|
||||||
|
|
||||||
# -- #15: .env не должен переопределять переменные окружения --
|
# -- #15: .env не должен переопределять переменные окружения --
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user