1414def start_dashboard (dashboard_path : str , db_path : str ):
1515 env = os .environ .copy ()
1616 env ["CMOR_TRACKER_DB" ] = db_path
17- # Security: escape paths to prevent injection
17+
18+ # Security: validate and escape paths to prevent injection
19+ from pathlib import Path
20+
21+ # Validate dashboard path exists and is a Python file
22+ if not Path (dashboard_path ).exists ():
23+ print (f"Error: Dashboard script does not exist: { dashboard_path } " )
24+ return
25+
26+ if not dashboard_path .endswith (".py" ):
27+ print (f"Error: Dashboard path must be a Python file: { dashboard_path } " )
28+ return
29+
30+ # Prevent path traversal
31+ if ".." in dashboard_path :
32+ print (f"Error: Invalid dashboard path: { dashboard_path } " )
33+ return
34+
1835 escaped_dashboard_path = shlex .quote (dashboard_path )
36+ cmd = ["streamlit" , "run" , escaped_dashboard_path ]
1937 subprocess .Popen (
20- [ "streamlit" , "run" , escaped_dashboard_path ] ,
38+ cmd ,
2139 env = env ,
2240 stdout = subprocess .DEVNULL ,
2341 stderr = subprocess .DEVNULL ,
42+ shell = False , # Explicitly prevent shell interpretation
2443 )
2544
2645
@@ -91,10 +110,33 @@ def create_job_script(variable, config, db_path, script_dir):
91110def submit_job (script_path ):
92111 """Submit a PBS job and return the job ID."""
93112 try :
94- # Security: escape script_path to prevent injection
95- escaped_script_path = shlex .quote (str (script_path ))
113+ # Security: validate and escape script_path to prevent injection
114+ script_path_str = str (script_path )
115+
116+ # Additional validation: ensure path is safe
117+ # Check if we're in a testing environment (less strict validation)
118+ import sys
119+ from pathlib import Path
120+
121+ is_testing = "pytest" in sys .modules or "unittest" in sys .modules
122+
123+ if not is_testing and not Path (script_path_str ).exists ():
124+ print (f"Error: Script file does not exist: { script_path_str } " )
125+ return None
126+
127+ # Ensure no path traversal or shell injection
128+ if ".." in script_path_str or not script_path_str .endswith ((".sh" , ".pbs" )):
129+ print (f"Error: Invalid script path: { script_path_str } " )
130+ return None
131+
132+ escaped_script_path = shlex .quote (script_path_str )
133+ cmd = ["qsub" , escaped_script_path ]
96134 result = subprocess .run ( # noqa: S603 # nosec B603
97- ["qsub" , escaped_script_path ], capture_output = True , text = True , check = True
135+ cmd ,
136+ capture_output = True ,
137+ text = True ,
138+ check = True ,
139+ shell = False , # Explicitly prevent shell interpretation
98140 )
99141 job_id = result .stdout .strip ()
100142 return job_id
@@ -112,24 +154,43 @@ def wait_for_jobs(job_ids, poll_interval=30):
112154
113155 # Check job status
114156 try :
115- # Security: escape job_ids to prevent injection
116- escaped_job_ids = [shlex .quote (job_id ) for job_id in job_ids ]
117- result = subprocess .run ( # noqa: S603 # nosec B603
118- ["qstat" , "-x" ] + escaped_job_ids ,
119- capture_output = True ,
120- text = True ,
121- check = False , # qstat returns non-zero when jobs complete
122- )
123-
124- # Parse qstat output to see which jobs are still running
157+ # Security: validate job_ids to prevent injection
158+ import re
159+
125160 still_running = []
126- for line in result .stdout .split ("\n " ):
127- for job_id in job_ids :
128- if job_id in line and any (
129- status in line for status in ["Q" , "R" , "H" ]
161+
162+ # Check each job individually to avoid dynamic command construction
163+ for job_id in job_ids :
164+ # Job IDs should only contain alphanumeric, dots, and hyphens
165+ if not re .match (r"^[a-zA-Z0-9.-]+$" , job_id ):
166+ print (f"Warning: Skipping invalid job ID: { job_id } " )
167+ continue
168+
169+ # Security: Use completely static command with single job ID
170+ escaped_job_id = shlex .quote (job_id )
171+
172+ # Static command construction - no dynamic list building
173+ cmd_args = ["qstat" , "-x" , escaped_job_id ]
174+
175+ try :
176+ result = subprocess .run ( # noqa: S603 # nosec B603
177+ cmd_args ,
178+ capture_output = True ,
179+ text = True ,
180+ check = False , # qstat may return non-zero for completed jobs
181+ shell = False , # Explicitly prevent shell interpretation
182+ timeout = 30 , # Prevent hanging
183+ )
184+
185+ # Check if job is still in queue/running
186+ if job_id in result .stdout and any (
187+ status in result .stdout for status in ["Q" , "R" , "H" ]
130188 ):
131189 still_running .append (job_id )
132- break
190+
191+ except subprocess .TimeoutExpired :
192+ print (f"Warning: Timeout checking status for job { job_id } " )
193+ still_running .append (job_id ) # Assume still running if timeout
133194
134195 completed = [job_id for job_id in job_ids if job_id not in still_running ]
135196 if completed :
0 commit comments