@@ -767,7 +767,10 @@ class InputHandler:
767767 Normalizes all inputs to a local directory path for scanning.
768768 """
769769
770- def __init__ (self , transitive_budget : object | None = None ) -> None :
770+ def __init__ (
771+ self , transitive_budget : object | None = None , * , allow_git_credentials : bool = True
772+ ) -> None :
773+ self ._allow_git_credentials = allow_git_credentials
771774 self ._temp_dir : Path | None = None
772775 self ._transitive_budget = transitive_budget
773776 self .primary_file_path : str | None = None
@@ -1129,23 +1132,99 @@ def _resolve_tree_ref(self, repository_url: str, segments: list[str]) -> tuple[s
11291132 f"{ repository_url } ({ '/' .join (segments )} )"
11301133 )
11311134
1135+ def _git_invocation (self , url : str ) -> tuple [list [str ], dict [str , str ] | None , str ]:
1136+ """Build a Git policy for both ref discovery and cloning.
1137+
1138+ Remote MCP callers must not borrow the server's SSH keys, helpers,
1139+ authorization headers, URL rewrites, client certificates, or netrc.
1140+ Trusted local callers retain the operator's configured Git identity.
1141+ """
1142+ if self ._allow_git_credentials :
1143+ return ["git" ], None , url
1144+ parsed = urlparse (url )
1145+ if parsed .scheme != "https" or parsed .username is not None or parsed .password is not None :
1146+ raise ValueError ("Untrusted Git scans require an unauthenticated HTTPS URL" )
1147+ if parsed .hostname in {"gitlab.com" , "bitbucket.org" }:
1148+ # Browser URLs need the Git endpoint without following a redirect
1149+ # whose destination has not passed the host/IP checks.
1150+ path = parsed .path .rstrip ("/" )
1151+ if path and not path .endswith (".git" ):
1152+ path += ".git"
1153+ url = parsed ._replace (path = path ).geturl ()
1154+ isolated_home = self ._get_temp_dir () / "git-home"
1155+ isolated_home .mkdir (mode = 0o700 , exist_ok = True )
1156+ env = {
1157+ key : value
1158+ for key , value in os .environ .items ()
1159+ if key .upper ()
1160+ in {
1161+ "PATH" ,
1162+ "SYSTEMROOT" ,
1163+ "WINDIR" ,
1164+ "TEMP" ,
1165+ "TMP" ,
1166+ "TMPDIR" ,
1167+ "HTTP_PROXY" ,
1168+ "HTTPS_PROXY" ,
1169+ "ALL_PROXY" ,
1170+ "NO_PROXY" ,
1171+ "GIT_SSL_CAINFO" ,
1172+ "GIT_SSL_CAPATH" ,
1173+ "SSL_CERT_FILE" ,
1174+ "SSL_CERT_DIR" ,
1175+ "CURL_CA_BUNDLE" ,
1176+ }
1177+ }
1178+ env .update (
1179+ HOME = str (isolated_home ),
1180+ USERPROFILE = str (isolated_home ),
1181+ XDG_CONFIG_HOME = str (isolated_home ),
1182+ GIT_CONFIG_GLOBAL = os .devnull ,
1183+ GIT_CONFIG_SYSTEM = os .devnull ,
1184+ GIT_CONFIG_NOSYSTEM = "1" ,
1185+ GIT_TERMINAL_PROMPT = "0" ,
1186+ GIT_ALLOW_PROTOCOL = "https" ,
1187+ GIT_CEILING_DIRECTORIES = str (isolated_home .parent ),
1188+ )
1189+ return (
1190+ [
1191+ "git" ,
1192+ "-C" ,
1193+ str (isolated_home ),
1194+ "-c" ,
1195+ "credential.helper=" ,
1196+ "-c" ,
1197+ "core.askPass=" ,
1198+ "-c" ,
1199+ f"core.hooksPath={ os .devnull } " ,
1200+ "-c" ,
1201+ "http.extraHeader=" ,
1202+ "-c" ,
1203+ "http.followRedirects=false" ,
1204+ ],
1205+ env ,
1206+ url ,
1207+ )
1208+
11321209 def _list_remote_refs (self , repository_url : str ) -> set [str ]:
11331210 """Return the branch/tag names advertised by the remote repository.
11341211
11351212 Bounded by the ingest deadline; the host allowlist and private-IP
11361213 checks from URL validation apply.
11371214 """
1215+ git_argv , git_env , repository_url = self ._git_invocation (repository_url )
11381216 self ._validate_url_host (repository_url , ALLOWED_GIT_HOSTS )
11391217 deadline = self ._deadline ()
11401218 self ._check_deadline (deadline , "git" )
11411219 timeout = max (1.0 , deadline - monotonic ())
11421220 try :
11431221 process = subprocess .run (
1144- ["git" , "ls-remote" , repository_url ],
1222+ [* git_argv , "ls-remote" , repository_url ],
11451223 stdout = subprocess .PIPE ,
11461224 stderr = subprocess .DEVNULL ,
11471225 timeout = timeout ,
11481226 check = False ,
1227+ ** ({"env" : git_env } if git_env is not None else {}),
11491228 )
11501229 except subprocess .TimeoutExpired as exc :
11511230 raise IngestLimitExceededError ("Git ref listing exceeded its time limit" ) from exc
@@ -1206,6 +1285,7 @@ def _clone_git(self, url: str, *, branch: str | None = None) -> Path:
12061285 self ._truncate ("byte_budget_exhausted" , "git" )
12071286 if remaining_artifacts is not None and remaining_artifacts <= 0 :
12081287 self ._truncate ("artifact_budget_exhausted" , "git" )
1288+ git_argv , git_env , url = self ._git_invocation (url )
12091289 self ._validate_url_host (url , ALLOWED_GIT_HOSTS )
12101290 deadline = self ._deadline ()
12111291 self ._check_deadline (deadline , "git" )
@@ -1225,6 +1305,7 @@ def _clone_git(self, url: str, *, branch: str | None = None) -> Path:
12251305 clone_command [6 :6 ] = ["--branch" , branch ]
12261306 if remaining_bytes is not None :
12271307 clone_command .insert (6 , f"--filter=blob:limit={ remaining_bytes } " )
1308+ clone_command [:1 ] = git_argv
12281309 process : subprocess .Popen [bytes ] | None = None
12291310 final_measurement : _TreeMeasurement | None = None
12301311 try :
@@ -1233,6 +1314,7 @@ def _clone_git(self, url: str, *, branch: str | None = None) -> Path:
12331314 stdout = subprocess .DEVNULL ,
12341315 stderr = subprocess .DEVNULL ,
12351316 shell = False ,
1317+ ** ({"env" : git_env } if git_env is not None else {}),
12361318 )
12371319 while True :
12381320 self ._check_deadline (deadline , "git" )
0 commit comments