From 1c78618cb37d38495a875f93ed7c53395ee7f43d Mon Sep 17 00:00:00 2001 From: Krishna Suravarapu <36037520+KrishnaSuravarapu@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:18:25 +0530 Subject: [PATCH 1/3] security(path-traversal): confine access-module clone/move to access_modules (F-021/F-023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Untrusted names — the folder derived from a git URL (clone_repo, F-023, CTO-4870) and directory names from a cloned repo (move_modules_from_cloned_repo, F-021, CTO-4868) — were concatenated into filesystem paths without sanitization, letting a crafted name escape ./Access/access_modules via '..'. Add safe_access_module_path(): strips path components with os.path.basename, rejects '', '.', '..', and asserts the realpath stays under the canonicalized access_modules base before the path is used as a clone target or rename dest. Legit module names are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/clone_access_modules.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/scripts/clone_access_modules.py b/scripts/clone_access_modules.py index 86c4db99..1175d7db 100644 --- a/scripts/clone_access_modules.py +++ b/scripts/clone_access_modules.py @@ -67,12 +67,36 @@ def get_repo_url_and_branch(formatted_git_arg): return url, target_branch +ACCESS_MODULES_DIR = "./Access/access_modules" + + +def safe_access_module_path(name): + """Resolve ``./Access/access_modules/`` while rejecting path traversal. + + ``name`` is untrusted: it is derived from a git URL (folder name) or read + from a cloned repository's directory listing. Strip any path components with + ``os.path.basename`` and confirm the resolved path stays inside + ``access_modules`` before it is used as a clone target or rename destination. + """ + safe_name = os.path.basename(str(name).strip().rstrip("/")) + if safe_name in ("", ".", ".."): + raise Exception(f"Unsafe access module name derived from {name!r}") + + target_path = os.path.join(ACCESS_MODULES_DIR, safe_name) + base_dir = os.path.realpath(ACCESS_MODULES_DIR) + resolved = os.path.realpath(target_path) + if resolved != base_dir and not resolved.startswith(base_dir + os.sep): + raise Exception(f"Refusing path outside access_modules: {name!r}") + return target_path + + def clone_repo(formatted_git_arg, retry_limit): """ Clone a single repo """ url, target_branch = get_repo_url_and_branch(formatted_git_arg) folder_name = url.split("/").pop()[:-4] - target_folder_path = "./Access/access_modules/" + folder_name + # F-023: folder_name comes from the (untrusted) git URL — sanitize before use + target_folder_path = safe_access_module_path(folder_name) retry_exception = None for clone_attempt in range(1, retry_limit + 1): @@ -123,9 +147,11 @@ def move_modules_from_cloned_repo(cloned_path): and each_path not in blacklist_paths ): try: + # F-021: each_path comes from the cloned repo — sanitize the + # rename destination so a crafted dir name can't escape. os.rename( cloned_path + "/" + each_path, - "./Access/access_modules/" + each_path + safe_access_module_path(each_path) ) except Exception as exception: logger.exception( From 5105dd02e9d025f5d2d7488cedcc8488cd84c79b Mon Sep 17 00:00:00 2001 From: Krishna Suravarapu <36037520+KrishnaSuravarapu@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:36:06 +0530 Subject: [PATCH 2/3] security(supply-chain): verify access-module integrity via pinned commit SHA (F-019) External access modules are cloned at runtime (and in CI) and imported directly, so a compromised upstream repo = RCE (CTO-4866, CVSS 8.1). clone_repo now treats a 40-char SHA after '#' as an integrity pin: after cloning it checks out that commit and asserts repo.head.commit.hexsha matches, aborting on mismatch. Unpinned modules log a warning to prompt migrating config.json git_urls to '#'. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/clone_access_modules.py | 34 +++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/scripts/clone_access_modules.py b/scripts/clone_access_modules.py index 1175d7db..fdbb6bbf 100644 --- a/scripts/clone_access_modules.py +++ b/scripts/clone_access_modules.py @@ -2,6 +2,7 @@ import logging import os +import re import shutil import sys import time @@ -92,20 +93,25 @@ def safe_access_module_path(name): def clone_repo(formatted_git_arg, retry_limit): """ Clone a single repo """ - url, target_branch = get_repo_url_and_branch(formatted_git_arg) + url, target_ref = get_repo_url_and_branch(formatted_git_arg) folder_name = url.split("/").pop()[:-4] # F-023: folder_name comes from the (untrusted) git URL — sanitize before use target_folder_path = safe_access_module_path(folder_name) + # F-019: if the ref after '#' is a full 40-char commit SHA, treat it as an + # integrity pin (verified below) rather than a branch to clone. + pinned_sha = target_ref if (target_ref and re.fullmatch(r"[0-9a-fA-F]{40}", target_ref)) else None + retry_exception = None + repo = None for clone_attempt in range(1, retry_limit + 1): try: logger.info("Cloning Repo") - if target_branch: - Repo.clone_from(url, target_folder_path, branch=target_branch) + if target_ref and not pinned_sha: + repo = Repo.clone_from(url, target_folder_path, branch=target_ref) else: - Repo.clone_from(url, target_folder_path) + repo = Repo.clone_from(url, target_folder_path) except (GitCommandError, Exception) as exception: sleep_time = 10 * clone_attempt logger.error( @@ -128,6 +134,26 @@ def clone_repo(formatted_git_arg, retry_limit): logger.exception("Max retry count reached while cloning repo") raise retry_exception + # F-019: integrity verification. External modules are cloned at runtime and + # imported directly, so a compromised upstream = RCE. If a commit SHA was + # pinned, check it out and assert HEAD matches; abort on mismatch. Otherwise + # warn that the module could not be integrity-verified. + if pinned_sha: + repo.git.checkout(pinned_sha) + actual_sha = repo.head.commit.hexsha + if actual_sha.lower() != pinned_sha.lower(): + raise Exception( + f"Integrity check failed for {url}: expected commit " + f"{pinned_sha}, got {actual_sha}" + ) + logger.info("Verified access module %s at pinned commit %s", url, pinned_sha) + else: + logger.warning( + "Access module %s is not pinned to a commit SHA; runtime integrity " + "cannot be verified (F-019). Pin via '#<40-char-commit-sha>'.", + url, + ) + return target_folder_path From ee20873a8bea1a3c9be099a0dfaa61351e4e3dde Mon Sep 17 00:00:00 2001 From: Krishna Suravarapu <36037520+KrishnaSuravarapu@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:52:09 +0530 Subject: [PATCH 3/3] refactor(f-019): extract SHA-pin + integrity helpers to satisfy lint Fix CI lint failures on the F-019 change: - R901 (clone_repo too complex, 11): move the pinned-SHA detection and the integrity verification into _pinned_commit_sha() and verify_module_integrity(). - E501 (line >100): the pinned_sha regex line now lives in the helper. Behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/clone_access_modules.py | 57 ++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/scripts/clone_access_modules.py b/scripts/clone_access_modules.py index fdbb6bbf..feb82a29 100644 --- a/scripts/clone_access_modules.py +++ b/scripts/clone_access_modules.py @@ -91,6 +91,38 @@ def safe_access_module_path(name): return target_path +def _pinned_commit_sha(target_ref): + """Return target_ref when it is a full 40-char commit SHA (an integrity pin).""" + if target_ref and re.fullmatch(r"[0-9a-fA-F]{40}", target_ref): + return target_ref + return None + + +def verify_module_integrity(repo, url, pinned_sha): + """F-019: verify a cloned module against its pinned commit SHA. + + External modules are cloned at runtime and imported directly, so a + compromised upstream = RCE. If a commit SHA was pinned, check it out and + assert HEAD matches, aborting on mismatch; otherwise warn that integrity + could not be verified. + """ + if not pinned_sha: + logger.warning( + "Access module %s is not pinned to a commit SHA; runtime integrity " + "cannot be verified (F-019). Pin via '#<40-char-commit-sha>'.", + url, + ) + return + repo.git.checkout(pinned_sha) + actual_sha = repo.head.commit.hexsha + if actual_sha.lower() != pinned_sha.lower(): + raise Exception( + f"Integrity check failed for {url}: expected commit " + f"{pinned_sha}, got {actual_sha}" + ) + logger.info("Verified access module %s at pinned commit %s", url, pinned_sha) + + def clone_repo(formatted_git_arg, retry_limit): """ Clone a single repo """ url, target_ref = get_repo_url_and_branch(formatted_git_arg) @@ -99,9 +131,8 @@ def clone_repo(formatted_git_arg, retry_limit): # F-023: folder_name comes from the (untrusted) git URL — sanitize before use target_folder_path = safe_access_module_path(folder_name) - # F-019: if the ref after '#' is a full 40-char commit SHA, treat it as an - # integrity pin (verified below) rather than a branch to clone. - pinned_sha = target_ref if (target_ref and re.fullmatch(r"[0-9a-fA-F]{40}", target_ref)) else None + # F-019: a full 40-char SHA after '#' is an integrity pin, not a branch. + pinned_sha = _pinned_commit_sha(target_ref) retry_exception = None repo = None @@ -134,25 +165,7 @@ def clone_repo(formatted_git_arg, retry_limit): logger.exception("Max retry count reached while cloning repo") raise retry_exception - # F-019: integrity verification. External modules are cloned at runtime and - # imported directly, so a compromised upstream = RCE. If a commit SHA was - # pinned, check it out and assert HEAD matches; abort on mismatch. Otherwise - # warn that the module could not be integrity-verified. - if pinned_sha: - repo.git.checkout(pinned_sha) - actual_sha = repo.head.commit.hexsha - if actual_sha.lower() != pinned_sha.lower(): - raise Exception( - f"Integrity check failed for {url}: expected commit " - f"{pinned_sha}, got {actual_sha}" - ) - logger.info("Verified access module %s at pinned commit %s", url, pinned_sha) - else: - logger.warning( - "Access module %s is not pinned to a commit SHA; runtime integrity " - "cannot be verified (F-019). Pin via '#<40-char-commit-sha>'.", - url, - ) + verify_module_integrity(repo, url, pinned_sha) return target_folder_path