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,29 @@ 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+ from pathlib import Path
118+
119+ if not Path (script_path_str ).exists ():
120+ print (f"Error: Script file does not exist: { script_path_str } " )
121+ return None
122+
123+ # Ensure no path traversal or shell injection
124+ if ".." in script_path_str or not script_path_str .endswith ((".sh" , ".pbs" )):
125+ print (f"Error: Invalid script path: { script_path_str } " )
126+ return None
127+
128+ escaped_script_path = shlex .quote (script_path_str )
129+ cmd = ["qsub" , escaped_script_path ]
96130 result = subprocess .run ( # noqa: S603 # nosec B603
97- ["qsub" , escaped_script_path ], capture_output = True , text = True , check = True
131+ cmd ,
132+ capture_output = True ,
133+ text = True ,
134+ check = True ,
135+ shell = False , # Explicitly prevent shell interpretation
98136 )
99137 job_id = result .stdout .strip ()
100138 return job_id
@@ -112,24 +150,43 @@ def wait_for_jobs(job_ids, poll_interval=30):
112150
113151 # Check job status
114152 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
153+ # Security: validate job_ids to prevent injection
154+ import re
155+
125156 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" ]
157+
158+ # Check each job individually to avoid dynamic command construction
159+ for job_id in job_ids :
160+ # Job IDs should only contain alphanumeric, dots, and hyphens
161+ if not re .match (r"^[a-zA-Z0-9.-]+$" , job_id ):
162+ print (f"Warning: Skipping invalid job ID: { job_id } " )
163+ continue
164+
165+ # Security: Use completely static command with single job ID
166+ escaped_job_id = shlex .quote (job_id )
167+
168+ # Static command construction - no dynamic list building
169+ cmd_args = ["qstat" , "-x" , escaped_job_id ]
170+
171+ try :
172+ result = subprocess .run ( # noqa: S603 # nosec B603
173+ cmd_args ,
174+ capture_output = True ,
175+ text = True ,
176+ check = False , # qstat may return non-zero for completed jobs
177+ shell = False , # Explicitly prevent shell interpretation
178+ timeout = 30 , # Prevent hanging
179+ )
180+
181+ # Check if job is still in queue/running
182+ if job_id in result .stdout and any (
183+ status in result .stdout for status in ["Q" , "R" , "H" ]
130184 ):
131185 still_running .append (job_id )
132- break
186+
187+ except subprocess .TimeoutExpired :
188+ print (f"Warning: Timeout checking status for job { job_id } " )
189+ still_running .append (job_id ) # Assume still running if timeout
133190
134191 completed = [job_id for job_id in job_ids if job_id not in still_running ]
135192 if completed :
0 commit comments