@@ -1027,7 +1027,11 @@ def extension_add(
10271027 from specify_cli ._download_security import read_response_limited as _read_response_limited
10281028 from specify_cli .authentication .http import open_url as _open_url
10291029
1030- with _open_url (from_url , timeout = 60 ) as response :
1030+ with _open_url (
1031+ from_url ,
1032+ timeout = 60 ,
1033+ strict_redirects = True ,
1034+ ) as response :
10311035 zip_data = _read_response_limited (
10321036 response ,
10331037 error_type = ExtensionError ,
@@ -2479,6 +2483,7 @@ def _validate_and_install_local(yaml_path: Path, source_label: str) -> None:
24792483 # Try as URL (http/https)
24802484 if source .startswith ("http://" ) or source .startswith ("https://" ):
24812485 from functools import partial
2486+ from urllib .parse import urlparse as _urlparse
24822487
24832488 from specify_cli ._download_security import read_response_limited as _read_response_limited
24842489 from specify_cli .authentication .http import open_url as _open_url
@@ -2510,7 +2515,17 @@ def _validate_and_install_local(yaml_path: Path, source_label: str) -> None:
25102515 ) as resp :
25112516 final_url = resp .geturl ()
25122517 if not is_https_or_localhost_http (final_url ):
2513- console .print (f"[red]Error:[/red] URL redirected to non-HTTPS: { final_url } " )
2518+ final_parsed = _urlparse (final_url )
2519+ if not final_parsed .hostname :
2520+ console .print (
2521+ f"[red]Error:[/red] URL redirected to a URL with no hostname: { final_url } "
2522+ )
2523+ else :
2524+ console .print (
2525+ "[red]Error:[/red] URL redirected to a URL without HTTPS "
2526+ "(HTTP is allowed only for localhost, 127.0.0.1, and ::1): "
2527+ f"{ final_url } "
2528+ )
25142529 raise typer .Exit (1 )
25152530 with tempfile .NamedTemporaryFile (suffix = ".yml" , delete = False ) as tmp :
25162531 tmp .write (
@@ -2587,6 +2602,7 @@ def _validate_and_install_local(yaml_path: Path, source_label: str) -> None:
25872602
25882603 try :
25892604 from functools import partial
2605+ from urllib .parse import urlparse as _urlparse
25902606
25912607 from specify_cli .authentication .http import open_url as _open_url
25922608 from specify_cli ._github_http import resolve_github_release_asset_api_url as _resolve_gh_asset
@@ -2613,9 +2629,17 @@ def _validate_and_install_local(yaml_path: Path, source_label: str) -> None:
26132629 if workflow_dir .exists ():
26142630 import shutil
26152631 shutil .rmtree (workflow_dir , ignore_errors = True )
2616- console .print (
2617- f"[red]Error:[/red] Workflow '{ source } ' redirected to non-HTTPS URL: { final_url } "
2618- )
2632+ final_parsed = _urlparse (final_url )
2633+ if not final_parsed .hostname :
2634+ console .print (
2635+ f"[red]Error:[/red] Workflow '{ source } ' redirected to a URL with no hostname: { final_url } "
2636+ )
2637+ else :
2638+ console .print (
2639+ f"[red]Error:[/red] Workflow '{ source } ' redirected to a URL without HTTPS "
2640+ "(HTTP is allowed only for localhost, 127.0.0.1, and ::1): "
2641+ f"{ final_url } "
2642+ )
26192643 raise typer .Exit (1 )
26202644 workflow_file .write_bytes (
26212645 _read_response_limited (
@@ -3052,20 +3076,28 @@ def workflow_step_add(
30523076 def _safe_fetch (url : str ) -> bytes :
30533077 parsed = urlparse (url )
30543078 is_localhost = parsed .hostname in ("localhost" , "127.0.0.1" , "::1" )
3055- if parsed .scheme != "https" and not (parsed .scheme == "http" and is_localhost ):
3056- raise ValueError (f"Refusing to fetch from non-HTTPS URL: { url } " )
30573079 if not parsed .hostname :
30583080 raise ValueError (f"Refusing to fetch from URL with no hostname: { url } " )
3059- with _open_url (url , timeout = 30 ) as resp :
3081+ if parsed .scheme != "https" and not (parsed .scheme == "http" and is_localhost ):
3082+ raise ValueError (
3083+ "Refusing to fetch from URL without HTTPS "
3084+ "(HTTP is allowed only for localhost, 127.0.0.1, and ::1): "
3085+ f"{ url } "
3086+ )
3087+ with _open_url (url , timeout = 30 , strict_redirects = True ) as resp :
30603088 final_url = resp .geturl ()
30613089 final_parsed = urlparse (final_url )
30623090 final_is_localhost = final_parsed .hostname in ("localhost" , "127.0.0.1" , "::1" )
3091+ if not final_parsed .hostname :
3092+ raise ValueError (f"Redirect to URL with no hostname: { final_url } " )
30633093 if final_parsed .scheme != "https" and not (
30643094 final_parsed .scheme == "http" and final_is_localhost
30653095 ):
3066- raise ValueError (f"Redirect to non-HTTPS URL: { final_url } " )
3067- if not final_parsed .hostname :
3068- raise ValueError (f"Redirect to URL with no hostname: { final_url } " )
3096+ raise ValueError (
3097+ "Redirect to URL without HTTPS "
3098+ "(HTTP is allowed only for localhost, 127.0.0.1, and ::1): "
3099+ f"{ final_url } "
3100+ )
30693101 return _read_response_limited (resp , label = f"workflow step { url } " )
30703102
30713103 _validate_step_id_or_exit (step_id )
0 commit comments