77from pathlib import Path
88import json
99import platform
10+ import re
1011from flask_cors import CORS , cross_origin
1112
13+ # Input validation pattern for safe names (alphanumeric, dash, underscore, slash, dot, space)
14+ SAFE_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_\-. ]+$' )
17+
18+ def validate_input (value , field_name , required = False ):
19+ """Validate that input contains only safe characters."""
20+ if value is None :
21+ if required :
22+ raise ValueError (f"Missing required field: { field_name } " )
23+ return True
24+ if not isinstance (value , str ):
25+ 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 } " )
28+ if len (value ) > 0 and not SAFE_INPUT_PATTERN .match (value ):
29+ raise ValueError (f"Invalid { field_name } : contains unsafe characters" )
30+ return True
31+
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+
1284cur_path = os .path .dirname (os .path .abspath (__file__ ))
1385concore_path = os .path .abspath (os .path .join (cur_path , '../../' ))
1486
@@ -306,20 +378,34 @@ def clear(dir):
306378def contribute ():
307379 try :
308380 data = request .json
309- PR_TITLE = data .get ('title' )
310- PR_BODY = data .get ('desc' )
311- AUTHOR_NAME = data .get ('auth' )
312- STUDY_NAME = data .get ('study' )
313- STUDY_NAME_PATH = data .get ('path' )
314- BRANCH_NAME = data .get ('branch' )
381+ PR_TITLE = data .get ('title' ) or ''
382+ PR_BODY = data .get ('desc' ) or ''
383+ AUTHOR_NAME = data .get ('auth' ) or ''
384+ STUDY_NAME = data .get ('study' ) or ''
385+ STUDY_NAME_PATH = data .get ('path' ) or ''
386+ BRANCH_NAME = data .get ('branch' ) or ''
387+
388+ # Validate all user inputs to prevent command injection
389+ # Strict validation for names/paths that go into command arguments
390+ validate_input (STUDY_NAME , 'study' , required = True )
391+ validate_input (STUDY_NAME_PATH , 'path' , required = True )
392+ validate_input (AUTHOR_NAME , 'auth' , required = True )
393+ validate_input (BRANCH_NAME , 'branch' , required = False )
394+
395+ # For PR title/body, allow more characters but enforce type/length
396+ validate_text_field (PR_TITLE , 'title' , max_length = 512 )
397+ validate_text_field (PR_BODY , 'desc' , max_length = 8192 )
398+
315399 if (platform .uname ()[0 ]== 'Windows' ):
316- proc = check_output (["contribute" ,STUDY_NAME ,STUDY_NAME_PATH ,AUTHOR_NAME ,BRANCH_NAME ,PR_TITLE ,PR_BODY ],cwd = concore_path ,shell = True )
400+ # Use cmd.exe /c to invoke contribute.bat on Windows
401+ 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 )
402+ output_string = proc .stdout
317403 else :
318404 if len (BRANCH_NAME )== 0 :
319405 proc = check_output ([r"./contribute" ,STUDY_NAME ,STUDY_NAME_PATH ,AUTHOR_NAME ],cwd = concore_path )
320406 else :
321407 proc = check_output ([r"./contribute" ,STUDY_NAME ,STUDY_NAME_PATH ,AUTHOR_NAME ,BRANCH_NAME ,PR_TITLE ,PR_BODY ],cwd = concore_path )
322- output_string = proc .decode ()
408+ output_string = proc .decode ()
323409 status = 200
324410 if output_string .find ("/pulls/" )!= - 1 :
325411 status = 200
@@ -328,6 +414,11 @@ def contribute():
328414 else :
329415 status = 400
330416 return jsonify ({'message' : output_string }),status
417+ except ValueError as e :
418+ return jsonify ({'message' : str (e )}), 400
419+ except subprocess .CalledProcessError as e :
420+ output_string = get_error_output (e )
421+ return jsonify ({'message' : output_string }), 501
331422 except Exception as e :
332423 output_string = "Some Error occured.Please try after some time"
333424 status = 501
@@ -397,18 +488,36 @@ def library(dir):
397488 dir_path = os .path .abspath (os .path .join (concore_path , dir_name ))
398489 filename = request .args .get ('filename' )
399490 library_path = request .args .get ('path' )
400- proc = 0
491+
492+ # Validate user inputs to prevent command injection
493+ try :
494+ # Use strict filename validation - no path separators or .. allowed
495+ validate_filename (filename , 'filename' , required = True )
496+ validate_input (library_path , 'path' , required = False )
497+ except ValueError as e :
498+ resp = jsonify ({'message' : str (e )})
499+ resp .status_code = 400
500+ return resp
501+
401502 if (library_path == None or library_path == '' ):
402503 library_path = r"../tools"
403- if (platform .uname ()[0 ]== 'Windows' ):
404- proc = subprocess .check_output ([r"..\library" , library_path , filename ],shell = True , cwd = dir_path )
405- else :
406- proc = subprocess .check_output ([r"../library" , library_path , filename ], cwd = dir_path )
407- if (proc != 0 ):
408- resp = jsonify ({'message' : proc .decode ("utf-8" )})
504+ try :
505+ if (platform .uname ()[0 ]== 'Windows' ):
506+ # Use cmd.exe /c to invoke library.bat on Windows
507+ result = subprocess .run (["cmd.exe" , "/c" , r"..\library.bat" , library_path , filename ], cwd = dir_path , check = True , capture_output = True , text = True )
508+ proc = result .stdout
509+ else :
510+ proc = subprocess .check_output ([r"../library" , library_path , filename ], cwd = dir_path )
511+ proc = proc .decode ("utf-8" )
512+ resp = jsonify ({'message' : proc })
409513 resp .status_code = 201
410514 return resp
411- else :
515+ except subprocess .CalledProcessError as e :
516+ error_output = get_error_output (e )
517+ resp = jsonify ({'message' : f'Command execution failed: { error_output } ' })
518+ resp .status_code = 500
519+ return resp
520+ except Exception as e :
412521 resp = jsonify ({'message' : 'There is an Error' })
413522 resp .status_code = 500
414523 return resp
0 commit comments