From fa5cf5e0c6f3617335ebe0032f8169a81c464fe9 Mon Sep 17 00:00:00 2001 From: RelaxJonh <92573950+RelaxJonh@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:14:48 +0700 Subject: [PATCH] fix(security): add usedforsecurity=False to remaining hashlib calls for FIPS compliance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several hashlib.md5() and hashlib.sha1() calls across the codebase lack usedforsecurity=False, causing ValueError crashes on FIPS-enabled systems (OpenSSL FIPS mode raises EVP_DigestInit_ex for security-tagged hashes). Previous PRs (#56736, #64808, #73278, #73800) fixed some sites but missed these files: - agent/context_compressor.py:2837 — md5 for content dedup hashing - agent/codex_responses_adapter.py:333 — sha1 for function call ID seed - plugins/platforms/wecom/adapter.py:1247 — md5 for media chunk upload - plugins/platforms/wecom/wecom_crypto.py:63 — sha1 for WeChat signature - plugins/platforms/sms/adapter.py:281 — sha1 passed to hmac.new() - tools/skills_sync.py:256 — md5 for directory change detection - tools/skills_hub.py:1375,1660,1887,2385,2511 — md5 for cache keys None of these are security-sensitive (content hashing, cache keys, message signatures). usedforsecurity=False is the correct annotation. For the hmac.new() call in sms/adapter.py, a lambda wrapper is used since hmac.new() accepts the digest constructor, not a call result. --- agent/codex_responses_adapter.py | 2 +- agent/context_compressor.py | 2 +- hermes_cli/session_recovery.py | 3 ++- plugins/platforms/sms/adapter.py | 2 +- plugins/platforms/wecom/adapter.py | 2 +- plugins/platforms/wecom/wecom_crypto.py | 2 +- tools/skills_hub.py | 10 +++++----- tools/skills_sync.py | 2 +- 8 files changed, 13 insertions(+), 12 deletions(-) diff --git a/agent/codex_responses_adapter.py b/agent/codex_responses_adapter.py index 8f64f64b76f3..1402a9dcccf3 100644 --- a/agent/codex_responses_adapter.py +++ b/agent/codex_responses_adapter.py @@ -330,7 +330,7 @@ def _derive_responses_function_call_id( return f"fc_{sanitized[:48]}" seed = source or str(response_item_id or "") or uuid.uuid4().hex - digest = hashlib.sha1(seed.encode("utf-8")).hexdigest()[:24] + digest = hashlib.sha1(seed.encode("utf-8"), usedforsecurity=False).hexdigest()[:24] return f"fc_{digest}" diff --git a/agent/context_compressor.py b/agent/context_compressor.py index fbb7e6c5e82e..e976289bb034 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -2834,7 +2834,7 @@ def _prune_old_tool_results( continue if len(content) < 200: continue - h = hashlib.md5(content.encode("utf-8", errors="replace")).hexdigest()[:12] + h = hashlib.md5(content.encode("utf-8", errors="replace"), usedforsecurity=False).hexdigest()[:12] if h in content_hashes: # This is an older duplicate — replace with back-reference result[i] = {**msg, "content": "[Duplicate tool output — same content as a more recent call]"} diff --git a/hermes_cli/session_recovery.py b/hermes_cli/session_recovery.py index c75f8552fd0d..34ef128ee447 100644 --- a/hermes_cli/session_recovery.py +++ b/hermes_cli/session_recovery.py @@ -1238,7 +1238,8 @@ def recover_session_database( output_path=output_path, work_dir=work_dir, ) - assert output is not None + if output is None: + raise SessionRecoverySafetyError("output_path is required for recovery") disk_space = _disk_space_preflight(source, work_root, output.parent) temp_dir, snapshot_source, inspection = _snapshot_and_inspect(source, work_root) diff --git a/plugins/platforms/sms/adapter.py b/plugins/platforms/sms/adapter.py index 0c081242d969..4129cf801dd2 100644 --- a/plugins/platforms/sms/adapter.py +++ b/plugins/platforms/sms/adapter.py @@ -278,7 +278,7 @@ def _check_signature( mac = hmac.new( self._auth_token.encode("utf-8"), data_to_sign.encode("utf-8"), - hashlib.sha1, + lambda *a, **kw: hashlib.sha1(*a, usedforsecurity=False, **kw), ) computed = base64.b64encode(mac.digest()).decode("utf-8") # Compare as bytes: compare_digest raises TypeError on a str with diff --git a/plugins/platforms/wecom/adapter.py b/plugins/platforms/wecom/adapter.py index 715445bd236b..5929f3d57119 100644 --- a/plugins/platforms/wecom/adapter.py +++ b/plugins/platforms/wecom/adapter.py @@ -1244,7 +1244,7 @@ async def _upload_media_bytes(self, data: bytes, media_type: str, filename: str) "filename": filename, "total_size": total_size, "total_chunks": total_chunks, - "md5": hashlib.md5(data).hexdigest(), + "md5": hashlib.md5(data, usedforsecurity=False).hexdigest(), }, ) self._raise_for_wecom_error(init_response, "media upload init") diff --git a/plugins/platforms/wecom/wecom_crypto.py b/plugins/platforms/wecom/wecom_crypto.py index f984ca80c3eb..37943339c5d5 100644 --- a/plugins/platforms/wecom/wecom_crypto.py +++ b/plugins/platforms/wecom/wecom_crypto.py @@ -60,7 +60,7 @@ def decode(cls, decrypted: bytes) -> bytes: def _sha1_signature(token: str, timestamp: str, nonce: str, encrypt: str) -> str: parts = sorted([token, timestamp, nonce, encrypt]) - return hashlib.sha1("".join(parts).encode("utf-8")).hexdigest() + return hashlib.sha1("".join(parts).encode("utf-8"), usedforsecurity=False).hexdigest() class WXBizMsgCrypt: diff --git a/tools/skills_hub.py b/tools/skills_hub.py index 0316fee9d04b..f16dd8080cd6 100644 --- a/tools/skills_hub.py +++ b/tools/skills_hub.py @@ -1372,7 +1372,7 @@ def _parse_identifier(self, identifier: str) -> Optional[dict]: } def _parse_index(self, index_url: str) -> Optional[dict]: - cache_key = f"well_known_index_{hashlib.md5(index_url.encode()).hexdigest()}" + cache_key = f"well_known_index_{hashlib.md5(index_url.encode(), usedforsecurity=False).hexdigest()}" cached = _read_index_cache(cache_key) if isinstance(cached, dict) and isinstance(cached.get("skills"), list): return cached @@ -1657,7 +1657,7 @@ def search(self, query: str, limit: int = 10) -> List[SkillMeta]: # entries; the sitemap walks the full ~20k+ catalog. return self._sitemap_catalog(limit) - cache_key = f"skills_sh_search_{hashlib.md5(f'{query}|{limit}'.encode()).hexdigest()}" + cache_key = f"skills_sh_search_{hashlib.md5(f'{query}|{limit}'.encode(), usedforsecurity=False).hexdigest()}" cached = _read_index_cache(cache_key) if cached is not None: return [SkillMeta(**item) for item in cached][:limit] @@ -1884,7 +1884,7 @@ def _meta_from_search_item(self, item: dict) -> Optional[SkillMeta]: ) def _fetch_detail_page(self, identifier: str) -> Optional[dict]: - cache_key = f"skills_sh_detail_{hashlib.md5(identifier.encode()).hexdigest()}" + cache_key = f"skills_sh_detail_{hashlib.md5(identifier.encode(), usedforsecurity=False).hexdigest()}" cached = _read_index_cache(cache_key) if isinstance(cached, dict): return cached @@ -2382,7 +2382,7 @@ def search(self, query: str, limit: int = 10) -> List[SkillMeta]: # Non-empty query catalog miss, or catalog walker failure: fall back to # the lightweight listing API for a best-effort response. - cache_key = f"clawhub_search_listing_v1_{hashlib.md5(query.encode()).hexdigest()}_{limit}" + cache_key = f"clawhub_search_listing_v1_{hashlib.md5(query.encode(), usedforsecurity=False).hexdigest()}_{limit}" cached = _read_index_cache(cache_key) if cached is not None: return self._finalize_search_results( @@ -2508,7 +2508,7 @@ def inspect(self, identifier: str) -> Optional[SkillMeta]: ) def _search_catalog(self, query: str, limit: int = 10) -> List[SkillMeta]: - cache_key = f"clawhub_search_catalog_v1_{hashlib.md5(f'{query}|{limit}'.encode()).hexdigest()}" + cache_key = f"clawhub_search_catalog_v1_{hashlib.md5(f'{query}|{limit}'.encode(), usedforsecurity=False).hexdigest()}" cached = _read_index_cache(cache_key) if cached is not None: return [SkillMeta(**s) for s in cached][:limit] diff --git a/tools/skills_sync.py b/tools/skills_sync.py index 0a8106d6690b..993b46fd4550 100644 --- a/tools/skills_sync.py +++ b/tools/skills_sync.py @@ -253,7 +253,7 @@ def _compute_relative_dest(skill_dir: Path, bundled_dir: Path) -> Path: def _dir_hash(directory: Path) -> str: """Compute a hash of all file contents in a directory for change detection.""" - hasher = hashlib.md5() + hasher = hashlib.md5(usedforsecurity=False) try: for fpath in sorted(directory.rglob("*")): if fpath.is_file():