@@ -20,7 +20,19 @@ def parse_args() -> argparse.Namespace:
2020 return parser .parse_args ()
2121
2222
23- def is_allowed (hostname : str , allowed : list [str ]) -> bool :
23+ def is_allowed (hostname : str , allowed : set [str ]) -> bool :
24+ """
25+ Check if the given hostname is allowed based on the provided set of allowed domains.
26+
27+ >>> is_allowed('example.com', {'example.com'})
28+ True
29+
30+ >>> is_allowed('sub.example.com', {'example.com'})
31+ False
32+
33+ >>> is_allowed('sub.example.com', {'*.example.com'})
34+ True
35+ """
2436 if not hostname :
2537 return False
2638
@@ -36,18 +48,30 @@ def is_allowed(hostname: str, allowed: list[str]) -> bool:
3648 if ip is not None :
3749 return False
3850
39- for domain in allowed :
40- if domain .startswith ('*.' ):
41- suffix = domain [2 :]
42- if host == suffix or host .endswith (f'.{ suffix } ' ):
43- return True
44- else :
45- if host == domain :
46- return True
51+ if host in allowed :
52+ return True
53+
54+ if any (host .endswith (f'.{ domain .lstrip (".*" )} ' ) for domain in allowed if domain .startswith ('*.' )):
55+ return True
56+
4757 return False
4858
4959
50- def walk_metadata (value , results , allowed ):
60+ def safe_under (base_dir : str , candidate_path : str ) -> str :
61+ """Return a canonical path that stays under base_dir, or raise ValueError."""
62+ safe_base = os .path .realpath (base_dir )
63+ safe_candidate = os .path .realpath (candidate_path )
64+
65+ try :
66+ if os .path .commonpath ([safe_base , safe_candidate ]) != safe_base :
67+ raise ValueError (f"Path escapes base directory: { candidate_path !r} " )
68+ except ValueError as exc :
69+ raise ValueError (f"Path escapes base directory: { candidate_path !r} " ) from exc
70+
71+ return safe_candidate
72+
73+
74+ def walk_metadata (value , results : set [str ], allowed : set [str ]) -> None :
5175 if isinstance (value , dict ):
5276 for item in value .values ():
5377 walk_metadata (item , results , allowed )
@@ -65,23 +89,25 @@ def walk_metadata(value, results, allowed):
6589def main () -> int :
6690 args = parse_args ()
6791 project_directory = args .project_directory
92+ safe_project_directory = os .path .realpath (project_directory , strict = True )
93+ safe_project_directory = safe_under (os .getcwd (), safe_project_directory )
6894 allowed_domains = args .allowed_domains
69- output_path = args .output_path
95+ safe_output_path = safe_under ( "/tmp" , os . path . realpath ( args .output_path , strict = True ))
7096
71- allowed = {}
97+ allowed : set [ str ] = set ()
7298 for raw_domain in allowed_domains .split (',' ):
7399 domain = raw_domain .strip ().lower ().rstrip ('.' )
74100 if domain :
75101 allowed .add (domain )
76102
77103 results : set [str ] = set ()
78104
79- for root , _ , files in os .walk (project_directory ):
105+ for root , _ , files in os .walk (safe_project_directory ):
80106 for file_name in files :
81107 if file_name != 'pyproject.toml' :
82108 continue
83109
84- manifest_path = os .path .join (root , file_name )
110+ manifest_path = safe_under ( safe_project_directory , os .path .join (root , file_name ) )
85111 try :
86112 with open (manifest_path , 'rb' ) as manifest_file :
87113 metadata = tomllib .load (manifest_file )
@@ -90,8 +116,27 @@ def main() -> int:
90116 continue
91117
92118 walk_metadata (metadata , results , allowed )
119+ # Also find any commented URLs in the pyproject.toml file
120+ try :
121+ with open (manifest_path , 'r' , encoding = 'utf-8' ) as manifest_file :
122+ for line in manifest_file :
123+ line = line .strip ()
124+ prefix , comment = line .split ('#' , maxsplit = 1 ) if '#' in line else (line , '' )
125+ if comment .strip ():
126+ comment = comment .strip ()
127+ if comment .startswith ('https://' ):
128+ host = urlsplit (comment ).hostname # handles extra at the end just fine
129+ if host and is_allowed (host , allowed ):
130+ results .add (comment )
131+ except OSError :
132+ print (f"Warning: Failed to read pyproject.toml at { manifest_path } " , file = sys .stderr )
133+ continue
134+
135+ output_directory = os .path .dirname (safe_output_path )
136+ if output_directory :
137+ os .makedirs (output_directory , exist_ok = True )
93138
94- with open (output_path , 'w' , encoding = 'utf-8' ) as output_file :
139+ with open (safe_output_path , 'w' , encoding = 'utf-8' ) as output_file :
95140 for url in sorted (results ):
96141 output_file .write (f'{ url } \n ' )
97142
0 commit comments