diff --git a/instsci/http_utils.py b/instsci/http_utils.py index 5ea463e..dc48c5e 100644 --- a/instsci/http_utils.py +++ b/instsci/http_utils.py @@ -3,20 +3,40 @@ import logging import os import time -import urllib3 import requests logger = logging.getLogger(__name__) -# Auto-detect local HTTP connector and disable SSL verification warnings. -if os.environ.get("HTTP_PROXY") or os.environ.get("HTTPS_PROXY") or \ - os.environ.get("http_proxy") or os.environ.get("https_proxy"): - # Local network connectors often use self-signed certificates. - _SSL_VERIFY = False - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -else: - _SSL_VERIFY = True +# TLS verification policy. +# +# Verification stays ON by default -- including behind an HTTP(S) proxy. The +# previous behaviour disabled certificate verification for *every* request as +# soon as any proxy env var was set, which silently exposed authenticated +# institutional sessions and publisher traffic to interception. To trust a +# self-signed connector CA, set REQUESTS_CA_BUNDLE (honoured natively by +# requests). Verification is only disabled by an explicit, deliberate opt-in. +_INSECURE_TLS_ENV = "INSTSCI_INSECURE_TLS" + + +def _resolve_ssl_verify() -> bool: + opt_in = os.environ.get(_INSECURE_TLS_ENV, "").strip().lower() + if opt_in in {"1", "true", "yes", "on"}: + import urllib3 + + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + logger.warning( + "%s is set: TLS certificate verification is DISABLED for all HTTP " + "requests. This exposes traffic -- including authenticated sessions " + "-- to interception. Prefer REQUESTS_CA_BUNDLE to trust a " + "self-signed connector CA instead.", + _INSECURE_TLS_ENV, + ) + return False + return True + + +_SSL_VERIFY = _resolve_ssl_verify() def request_with_retry( diff --git a/tests/test_http_utils_tls.py b/tests/test_http_utils_tls.py new file mode 100644 index 0000000..c1c36d4 --- /dev/null +++ b/tests/test_http_utils_tls.py @@ -0,0 +1,59 @@ +import importlib +import os +import unittest + +import instsci.http_utils as http_utils + +_PROXY_ENV = ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy") +_INSECURE_ENV = "INSTSCI_INSECURE_TLS" + + +class TlsVerifyPolicyTests(unittest.TestCase): + """TLS verification must stay on unless explicitly opted out. + + Regression guard: a proxy env var alone must never disable certificate + verification (which previously exposed authenticated sessions). + """ + + def setUp(self): + self._saved = {k: os.environ.get(k) for k in (*_PROXY_ENV, _INSECURE_ENV)} + for k in self._saved: + os.environ.pop(k, None) + + def tearDown(self): + for k, v in self._saved.items(): + if v is None: + os.environ.pop(k, None) + else: + os.environ[k] = v + importlib.reload(http_utils) + + def _verify(self): + return importlib.reload(http_utils)._SSL_VERIFY + + def test_verify_on_by_default(self): + self.assertTrue(self._verify()) + + def test_verify_stays_on_behind_proxy(self): + os.environ["HTTPS_PROXY"] = "http://127.0.0.1:8888" + self.assertTrue(self._verify()) + + def test_verify_disabled_only_with_explicit_optin(self): + os.environ[_INSECURE_ENV] = "1" + self.assertFalse(self._verify()) + + def test_optin_accepts_common_truthy_values(self): + for val in ("true", "YES", "on"): + with self.subTest(val=val): + os.environ[_INSECURE_ENV] = val + self.assertFalse(self._verify()) + + def test_optin_ignores_falsey_values(self): + for val in ("", "0", "false", "no"): + with self.subTest(val=val): + os.environ[_INSECURE_ENV] = val + self.assertTrue(self._verify()) + + +if __name__ == "__main__": + unittest.main()