From 25a6a6367f7277a929613d35830d633176bd710c Mon Sep 17 00:00:00 2001 From: yaodong-shen Date: Mon, 27 Jul 2026 10:05:41 +0800 Subject: [PATCH] fix(ai_agents): enforce TLS certificate verification Keep certificate and hostname verification enabled for affected cloud WebSocket clients. Add a repository-wide regression test and document the TLS policy. Fixes #2230 --- ai_agents/Taskfile.yml | 7 ++ .../fashionai/src/fashionai_client.py | 3 +- .../minimax_tts.py | 2 - .../stepfun_tts_python/stepfun_tts.py | 2 - .../xfyun_asr_bigmodel_python/recognition.py | 4 +- .../xfyun_asr_dialect_python/recognition.py | 4 +- .../extension/xfyun_asr_python/recognition.py | 4 +- ai_agents/tests/test_tls_verification.py | 67 +++++++++++++++++++ docs/ai/L1/08_security.md | 8 +++ 9 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 ai_agents/tests/test_tls_verification.py diff --git a/ai_agents/Taskfile.yml b/ai_agents/Taskfile.yml index 052dd855d3..ea17ef13ac 100644 --- a/ai_agents/Taskfile.yml +++ b/ai_agents/Taskfile.yml @@ -55,9 +55,16 @@ tasks: test: desc: run tests cmds: + - task: test-security - task: test-agent-extensions - task: test-server + test-security: + desc: run security invariant tests + internal: true + cmds: + - python -m pytest tests {{ .CLI_ARGS }} + test-server: desc: test server dir: ./server diff --git a/ai_agents/agents/ten_packages/extension/fashionai/src/fashionai_client.py b/ai_agents/agents/ten_packages/extension/fashionai/src/fashionai_client.py index 0a1d8f1f5a..a60b37d7f5 100644 --- a/ai_agents/agents/ten_packages/extension/fashionai/src/fashionai_client.py +++ b/ai_agents/agents/ten_packages/extension/fashionai/src/fashionai_client.py @@ -15,8 +15,7 @@ def __init__(self, ten_env, uri, service_id): self.ten_env = ten_env async def connect(self): - # pylint: disable=protected-access - ssl_context = ssl._create_unverified_context() + ssl_context = ssl.create_default_context() self.websocket = await websockets.connect(self.uri, ssl=ssl_context) asyncio.create_task( self.listen() diff --git a/ai_agents/agents/ten_packages/extension/minimax_tts_websocket_python/minimax_tts.py b/ai_agents/agents/ten_packages/extension/minimax_tts_websocket_python/minimax_tts.py index 8c8a3ca198..75f46cced2 100644 --- a/ai_agents/agents/ten_packages/extension/minimax_tts_websocket_python/minimax_tts.py +++ b/ai_agents/agents/ten_packages/extension/minimax_tts_websocket_python/minimax_tts.py @@ -174,8 +174,6 @@ async def _process_websocket(self) -> None: # Establish connection headers = {"Authorization": f"Bearer {self.config.key}"} ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE session_start_time = time.time() if self.ten_env: diff --git a/ai_agents/agents/ten_packages/extension/stepfun_tts_python/stepfun_tts.py b/ai_agents/agents/ten_packages/extension/stepfun_tts_python/stepfun_tts.py index f9e11c43aa..37801500c2 100644 --- a/ai_agents/agents/ten_packages/extension/stepfun_tts_python/stepfun_tts.py +++ b/ai_agents/agents/ten_packages/extension/stepfun_tts_python/stepfun_tts.py @@ -172,8 +172,6 @@ async def _process_websocket(self) -> None: # Establish connection headers = {"Authorization": f"Bearer {self.config.api_key}"} ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE session_start_time = time.time() if self.ten_env: diff --git a/ai_agents/agents/ten_packages/extension/xfyun_asr_bigmodel_python/recognition.py b/ai_agents/agents/ten_packages/extension/xfyun_asr_bigmodel_python/recognition.py index 9ba833909f..a8a28ac3cd 100644 --- a/ai_agents/agents/ten_packages/extension/xfyun_asr_bigmodel_python/recognition.py +++ b/ai_agents/agents/ten_packages/extension/xfyun_asr_bigmodel_python/recognition.py @@ -259,10 +259,8 @@ async def start(self, timeout=10): ws_url = self._create_url() self._log_debug(f"Connecting to: {ws_url}") - # Create SSL context that doesn't verify certificates (similar to original) + # Create an SSL context with standard certificate verification. ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE # Connect to WebSocket with timeout self.websocket = await websockets.connect( diff --git a/ai_agents/agents/ten_packages/extension/xfyun_asr_dialect_python/recognition.py b/ai_agents/agents/ten_packages/extension/xfyun_asr_dialect_python/recognition.py index 6c06186e44..f82e52e574 100644 --- a/ai_agents/agents/ten_packages/extension/xfyun_asr_dialect_python/recognition.py +++ b/ai_agents/agents/ten_packages/extension/xfyun_asr_dialect_python/recognition.py @@ -289,10 +289,8 @@ async def start(self, timeout=10): ws_url = self._create_url() self._log_debug(f"Connecting to: {ws_url}") - # Create SSL context that doesn't verify certificates (similar to original) + # Create an SSL context with standard certificate verification. ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE # Connect to WebSocket with timeout self.websocket = await websockets.connect( diff --git a/ai_agents/agents/ten_packages/extension/xfyun_asr_python/recognition.py b/ai_agents/agents/ten_packages/extension/xfyun_asr_python/recognition.py index 329e9071b0..fcf6dbde0f 100644 --- a/ai_agents/agents/ten_packages/extension/xfyun_asr_python/recognition.py +++ b/ai_agents/agents/ten_packages/extension/xfyun_asr_python/recognition.py @@ -233,10 +233,8 @@ async def start(self, timeout=10): ws_url = self._create_url() self.ten_env.log_info(f"Connecting to: {ws_url}") - # Create SSL context that doesn't verify certificates (similar to original) + # Create an SSL context with standard certificate verification. ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE # Connect to WebSocket with timeout self.websocket = await websockets.connect( diff --git a/ai_agents/tests/test_tls_verification.py b/ai_agents/tests/test_tls_verification.py new file mode 100644 index 0000000000..b4b06dea9b --- /dev/null +++ b/ai_agents/tests/test_tls_verification.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import ast +from pathlib import Path + +EXTENSIONS_ROOT = ( + Path(__file__).resolve().parents[1] + / "agents" + / "ten_packages" + / "extension" +) + + +def _attribute_name(node: ast.AST) -> str | None: + if not isinstance(node, ast.Attribute): + return None + if isinstance(node.value, ast.Name): + return f"{node.value.id}.{node.attr}" + return node.attr + + +def _find_insecure_tls_settings(path: Path) -> list[str]: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + violations = [] + + for node in ast.walk(tree): + if isinstance(node, ast.Call): + if _attribute_name(node.func) == "ssl._create_unverified_context": + violations.append( + f"{path.relative_to(EXTENSIONS_ROOT)}:{node.lineno} " + "uses an unverified SSL context" + ) + + if not isinstance(node, ast.Assign): + continue + + for target in node.targets: + if not isinstance(target, ast.Attribute): + continue + if target.attr == "check_hostname" and isinstance( + node.value, ast.Constant + ): + if node.value.value is False: + violations.append( + f"{path.relative_to(EXTENSIONS_ROOT)}:{node.lineno} " + "disables hostname verification" + ) + if ( + target.attr == "verify_mode" + and _attribute_name(node.value) == "ssl.CERT_NONE" + ): + violations.append( + f"{path.relative_to(EXTENSIONS_ROOT)}:{node.lineno} " + "disables certificate verification" + ) + + return violations + + +def test_production_extensions_do_not_disable_tls_verification(): + violations = [] + for path in EXTENSIONS_ROOT.rglob("*.py"): + if "tests" in path.parts: + continue + violations.extend(_find_insecure_tls_settings(path)) + + assert violations == [] diff --git a/docs/ai/L1/08_security.md b/docs/ai/L1/08_security.md index 18b68995e2..3739af9915 100644 --- a/docs/ai/L1/08_security.md +++ b/docs/ai/L1/08_security.md @@ -41,6 +41,14 @@ def to_str(self, sensitive_handling: bool = True) -> str: Never log raw API keys, tokens, or credentials. +## TLS Verification + +- Use `ssl.create_default_context()` for outbound TLS and WebSocket clients. +- Never set `check_hostname` to `False` or `verify_mode` to `ssl.CERT_NONE`. +- Never use `ssl._create_unverified_context()` for production connections. +- Add trusted private certificate authorities to the default context when a + deployment requires them; do not disable verification globally. + ## Server-Side Protections The Go server (`http_server.go`) implements: