1212
1313# Input validation pattern for safe names (alphanumeric, dash, underscore, slash, dot, space)
1414SAFE_INPUT_PATTERN = re .compile (r'^[a-zA-Z0-9_\-/. ]+$' )
15+ # Pattern for filenames - no path separators or .. allowed
16+ SAFE_FILENAME_PATTERN = re .compile (r'^[a-zA-Z0-9_\-. ]+$' )
1517
16- def validate_input (value , field_name ):
18+ def validate_input (value , field_name , required = False ):
1719 """Validate that input contains only safe characters."""
1820 if value is None :
21+ if required :
22+ raise ValueError (f"Missing required field: { field_name } " )
1923 return True
2024 if not isinstance (value , str ):
2125 raise ValueError (f"Invalid { field_name } : must be a string" )
26+ if required and len (value ) == 0 :
27+ raise ValueError (f"Missing required field: { field_name } " )
2228 if len (value ) > 0 and not SAFE_INPUT_PATTERN .match (value ):
2329 raise ValueError (f"Invalid { field_name } : contains unsafe characters" )
2430 return True
2531
32+ def validate_filename (value , field_name , required = False ):
33+ """Validate filename - no path separators or .. segments allowed."""
34+ if value is None :
35+ if required :
36+ raise ValueError (f"Missing required field: { field_name } " )
37+ return True
38+ if not isinstance (value , str ):
39+ raise ValueError (f"Invalid { field_name } : must be a string" )
40+ if required and len (value ) == 0 :
41+ raise ValueError (f"Missing required field: { field_name } " )
42+ # Reject path traversal attempts
43+ if '..' in value :
44+ raise ValueError (f"Invalid { field_name } : path traversal not allowed" )
45+ # Use basename to strip any path components
46+ basename = os .path .basename (value )
47+ if basename != value :
48+ raise ValueError (f"Invalid { field_name } : must be a filename, not a path" )
49+ if len (value ) > 0 and not SAFE_FILENAME_PATTERN .match (value ):
50+ raise ValueError (f"Invalid { field_name } : contains unsafe characters" )
51+ return True
52+
53+ def validate_text_field (value , field_name , max_length = None ):
54+ """Validate text fields like PR title/body - allow more characters but check type/length."""
55+ if value is None :
56+ return True
57+ if not isinstance (value , str ):
58+ raise ValueError (f"Invalid { field_name } : must be a string" )
59+ if max_length and len (value ) > max_length :
60+ raise ValueError (f"Invalid { field_name } : too long (max { max_length } characters)" )
61+ return True
62+
63+ def get_error_output (e ):
64+ """Extract error output from CalledProcessError, preferring stderr then output."""
65+ raw_output = None
66+ if hasattr (e , 'stderr' ) and e .stderr :
67+ raw_output = e .stderr
68+ elif hasattr (e , 'output' ) and e .output :
69+ raw_output = e .output
70+
71+ if raw_output is None :
72+ return "Command execution failed"
73+
74+ if isinstance (raw_output , bytes ):
75+ try :
76+ return raw_output .decode ('utf-8' , errors = 'replace' )
77+ except Exception :
78+ return str (raw_output )
79+ elif isinstance (raw_output , str ):
80+ return raw_output
81+ else :
82+ return str (raw_output )
83+
2684cur_path = os .path .dirname (os .path .abspath (__file__ ))
2785concore_path = os .path .abspath (os .path .join (cur_path , '../../' ))
2886
@@ -312,23 +370,27 @@ def clear(dir):
312370def contribute ():
313371 try :
314372 data = request .json
315- PR_TITLE = data .get ('title' )
316- PR_BODY = data .get ('desc' )
317- AUTHOR_NAME = data .get ('auth' )
318- STUDY_NAME = data .get ('study' )
319- STUDY_NAME_PATH = data .get ('path' )
320- BRANCH_NAME = data .get ('branch' )
373+ PR_TITLE = data .get ('title' ) or ''
374+ PR_BODY = data .get ('desc' ) or ''
375+ AUTHOR_NAME = data .get ('auth' ) or ''
376+ STUDY_NAME = data .get ('study' ) or ''
377+ STUDY_NAME_PATH = data .get ('path' ) or ''
378+ BRANCH_NAME = data .get ('branch' ) or ''
321379
322380 # Validate all user inputs to prevent command injection
323- validate_input (STUDY_NAME , 'study' )
324- validate_input (STUDY_NAME_PATH , 'path' )
325- validate_input (AUTHOR_NAME , 'auth' )
326- validate_input (BRANCH_NAME , 'branch' )
327- validate_input (PR_TITLE , 'title' )
328- validate_input (PR_BODY , 'desc' )
381+ # Strict validation for names/paths that go into command arguments
382+ validate_input (STUDY_NAME , 'study' , required = True )
383+ validate_input (STUDY_NAME_PATH , 'path' , required = True )
384+ validate_input (AUTHOR_NAME , 'auth' , required = True )
385+ validate_input (BRANCH_NAME , 'branch' , required = False )
386+
387+ # For PR title/body, allow more characters but enforce type/length
388+ validate_text_field (PR_TITLE , 'title' , max_length = 512 )
389+ validate_text_field (PR_BODY , 'desc' , max_length = 8192 )
329390
330391 if (platform .uname ()[0 ]== 'Windows' ):
331- proc = subprocess .run (["contribute" ,STUDY_NAME ,STUDY_NAME_PATH ,AUTHOR_NAME ,BRANCH_NAME ,PR_TITLE ,PR_BODY ],cwd = concore_path ,check = True ,capture_output = True ,text = True )
392+ # Use cmd.exe /c to invoke contribute.bat on Windows
393+ proc = subprocess .run (["cmd.exe" , "/c" , "contribute.bat" , STUDY_NAME , STUDY_NAME_PATH , AUTHOR_NAME , BRANCH_NAME , PR_TITLE , PR_BODY ], cwd = concore_path , check = True , capture_output = True , text = True )
332394 output_string = proc .stdout
333395 else :
334396 if len (BRANCH_NAME )== 0 :
@@ -347,7 +409,7 @@ def contribute():
347409 except ValueError as e :
348410 return jsonify ({'message' : str (e )}), 400
349411 except subprocess .CalledProcessError as e :
350- output_string = e . stderr if hasattr ( e , 'stderr' ) and e . stderr else "Command execution failed"
412+ output_string = get_error_output ( e )
351413 return jsonify ({'message' : output_string }), 501
352414 except Exception as e :
353415 output_string = "Some Error occured.Please try after some time"
@@ -397,19 +459,20 @@ def library(dir):
397459
398460 # Validate user inputs to prevent command injection
399461 try :
400- validate_input (filename , 'filename' )
401- validate_input (library_path , 'path' )
462+ # Use strict filename validation - no path separators or .. allowed
463+ validate_filename (filename , 'filename' , required = True )
464+ validate_input (library_path , 'path' , required = False )
402465 except ValueError as e :
403466 resp = jsonify ({'message' : str (e )})
404467 resp .status_code = 400
405468 return resp
406469
407- proc = 0
408470 if (library_path == None or library_path == '' ):
409471 library_path = r"../tools"
410472 try :
411473 if (platform .uname ()[0 ]== 'Windows' ):
412- result = subprocess .run ([r"..\library" , library_path , filename ], cwd = dir_path , check = True , capture_output = True , text = True )
474+ # Use cmd.exe /c to invoke library.bat on Windows
475+ result = subprocess .run (["cmd.exe" , "/c" , r"..\library.bat" , library_path , filename ], cwd = dir_path , check = True , capture_output = True , text = True )
413476 proc = result .stdout
414477 else :
415478 proc = subprocess .check_output ([r"../library" , library_path , filename ], cwd = dir_path )
@@ -418,7 +481,8 @@ def library(dir):
418481 resp .status_code = 201
419482 return resp
420483 except subprocess .CalledProcessError as e :
421- resp = jsonify ({'message' : 'Command execution failed' })
484+ error_output = get_error_output (e )
485+ resp = jsonify ({'message' : f'Command execution failed: { error_output } ' })
422486 resp .status_code = 500
423487 return resp
424488 except Exception as e :
0 commit comments