Core security operations for file and directory management Handles path validation, directory creation, and file operations
!! Core security operations for file and directory management !! Handles path validation, directory creation, and file operations module fortplot_security_core use, intrinsic :: iso_fortran_env, only: wp => real64, error_unit use fortplot_logging, only: log_error, log_warning, log_info use fortplot_system_runtime implicit none private public :: safe_create_directory public :: safe_remove_file public :: sanitize_filename public :: is_safe_path public :: get_test_output_path public :: check_path_exists ! Security-related constants integer, parameter :: MAX_PATH_LENGTH = 4096 integer, parameter :: MAX_NESTED_DIRS = 32 ! Maximum nested directory depth integer, parameter :: MAX_COMMAND_LENGTH = 1024 ! Maximum command line length integer, parameter :: SMALL_COMMAND_LENGTH = 512 ! Small command buffer size integer, parameter :: MAX_RETRIES = 99 ! Maximum file open retry attempts ! Control character boundaries integer, parameter :: CHAR_NULL = 0 ! NULL character integer, parameter :: CHAR_CTRL_END = 31 ! End of control characters integer, parameter :: CHAR_DEL = 127 ! DEL character ! SECURITY ENHANCEMENT: Comprehensive safe filename characters ! Allows scientific/technical filenames while blocking injection vectors character(len=*), parameter :: SAFE_FILENAME_CHARS = & 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./ \' contains !> Check if a file or directory exists function check_path_exists(path) result(exists) character(len=*), intent(in) :: path logical :: exists inquire(file=trim(path), exist=exists) end function check_path_exists !> Safely create directory without shell injection subroutine safe_create_directory(dir_path, success) character(len=*), intent(in) :: dir_path logical, intent(out) :: success if (.not. is_safe_path(dir_path)) then call log_error("Security: Invalid characters in directory path: " // trim(dir_path)) success = .false. return end if if (check_path_exists(dir_path)) then success = .true. return end if call try_create_directory(dir_path, success) call log_directory_creation_result(dir_path, success) end subroutine safe_create_directory !> Log the result of directory creation attempt subroutine log_directory_creation_result(dir_path, success) character(len=*), intent(in) :: dir_path logical, intent(in) :: success if (success) then call log_info("Directory created successfully: " // trim(dir_path)) else call log_error("Failed to create directory: " // trim(dir_path)) end if end subroutine log_directory_creation_result !> Attempt to create directory using multiple strategies subroutine try_create_directory(dir_path, success) character(len=*), intent(in) :: dir_path logical, intent(out) :: success ! Try creating the directory directly first call try_create_single_directory(dir_path, success) if (.not. success) then ! If direct creation fails, try creating parent directories first call create_parent_directories(dir_path, success) if (success) then call try_create_single_directory(dir_path, success) end if end if if (.not. success) then ! Final fallback: try system mkdir command call try_mkdir_command(success) end if end subroutine try_create_directory !> Try system mkdir command as fallback subroutine try_mkdir_command(success) logical, intent(out) :: success ! For security, we don't use execute_command_line ! This is just a placeholder - actual implementation uses safe system calls success = .false. end subroutine try_mkdir_command !> Create parent directories recursively subroutine create_parent_directories(dir_path, success) character(len=*), intent(in) :: dir_path logical, intent(out) :: success character(len=MAX_PATH_LENGTH), allocatable :: path_parts(:) integer :: num_parts allocate(path_parts(MAX_NESTED_DIRS)) call split_path_into_parts(dir_path, path_parts, num_parts) if (num_parts > MAX_NESTED_DIRS) then call log_error("Directory nesting too deep: " // trim(dir_path)) success = .false. return end if call create_directories_from_parts(path_parts, num_parts - 1, success) if (allocated(path_parts)) deallocate(path_parts) end subroutine create_parent_directories !> Split directory path into components subroutine split_path_into_parts(dir_path, path_parts, num_parts) character(len=*), intent(in) :: dir_path character(len=*), intent(out) :: path_parts(:) integer, intent(out) :: num_parts integer :: i, start, path_len character(len=len_trim(dir_path)) :: cleaned_path cleaned_path = trim(dir_path) path_len = len_trim(cleaned_path) num_parts = 0 start = 1 do i = 1, path_len if (cleaned_path(i:i) == '/') then if (i > start) then num_parts = num_parts + 1 if (num_parts <= size(path_parts)) then path_parts(num_parts) = cleaned_path(start:i-1) end if end if start = i + 1 end if end do ! Add final component if (start <= path_len) then num_parts = num_parts + 1 if (num_parts <= size(path_parts)) then path_parts(num_parts) = cleaned_path(start:path_len) end if end if end subroutine split_path_into_parts !> Create directories from path parts subroutine create_directories_from_parts(path_parts, num_parts, success) character(len=*), intent(in) :: path_parts(:) integer, intent(in) :: num_parts logical, intent(out) :: success character(len=MAX_PATH_LENGTH) :: current_path integer :: i logical :: create_success success = .true. current_path = "" do i = 1, num_parts call build_next_path_level(current_path, path_parts(i), i) if (.not. check_path_exists(current_path)) then call try_create_single_directory(current_path, create_success) if (.not. create_success) then success = .false. return end if end if end do end subroutine create_directories_from_parts !> Build the next path level subroutine build_next_path_level(current_path, next_part, level) character(len=*), intent(inout) :: current_path character(len=*), intent(in) :: next_part integer, intent(in) :: level if (level == 1) then current_path = trim(next_part) else if (len_trim(current_path) > 0) then current_path = trim(current_path) // "/" // trim(next_part) else current_path = trim(next_part) end if end if end subroutine build_next_path_level !> Try to create a single directory using Fortran I/O subroutine try_create_single_directory(dir_path, success) character(len=*), intent(in) :: dir_path logical, intent(out) :: success ! Use the create_directory_runtime function from system runtime call create_directory_runtime(trim(dir_path), success) end subroutine try_create_single_directory !> Safely remove file subroutine safe_remove_file(filename, success) character(len=*), intent(in) :: filename logical, intent(out) :: success if (.not. is_safe_path(filename)) then call log_error("Security: Invalid characters in filename: " // trim(filename)) success = .false. return end if if (.not. check_path_exists(filename)) then call log_warning("File does not exist: " // trim(filename)) success = .true. ! Consider non-existent file as successfully "removed" return end if call perform_file_removal(filename, success) end subroutine safe_remove_file !> Perform actual file removal subroutine perform_file_removal(filename, success) character(len=*), intent(in) :: filename logical, intent(out) :: success integer :: unit, iostat open(newunit=unit, file=trim(filename), iostat=iostat, status='old') if (iostat == 0) then close(unit, status='delete', iostat=iostat) success = (iostat == 0) else success = .false. end if end subroutine perform_file_removal !> Sanitize filename by removing dangerous characters function sanitize_filename(filename) result(safe_filename) character(len=*), intent(in) :: filename character(len=len(filename)) :: safe_filename integer :: i safe_filename = "" do i = 1, len_trim(filename) if (index(SAFE_FILENAME_CHARS, filename(i:i)) > 0) then safe_filename = trim(safe_filename) // filename(i:i) else safe_filename = trim(safe_filename) // "_" end if end do end function sanitize_filename !> Check if path is safe from injection attacks function is_safe_path(path) result(safe) character(len=*), intent(in) :: path logical :: safe safe = validate_path_length(path) .and. & validate_path_characters(path) .and. & validate_path_patterns(path) .and. & validate_system_paths(path) end function is_safe_path !> Validate path length function validate_path_length(path) result(valid) character(len=*), intent(in) :: path logical :: valid valid = len_trim(path) > 0 .and. len_trim(path) <= MAX_PATH_LENGTH if (.not. valid) then if (len_trim(path) == 0) then call log_error("Security: Empty path provided") else call log_error("Security: Path too long (max " // & trim(adjustl(int_to_str(MAX_PATH_LENGTH))) // " chars): " // trim(path)) end if end if end function validate_path_length !> Validate path characters function validate_path_characters(path) result(valid) character(len=*), intent(in) :: path logical :: valid integer :: i, char_code character :: c valid = .true. do i = 1, len_trim(path) c = path(i:i) char_code = ichar(c) if (is_control_character(c) .or. is_shell_injection_char(c)) then call log_error("Security: Dangerous character detected at position " // & trim(adjustl(int_to_str(i))) // " in path: " // trim(path)) valid = .false. return end if end do end function validate_path_characters !> Check if character is shell injection risk - COMPREHENSIVE SECURITY function is_shell_injection_char(char) result(dangerous) character, intent(in) :: char logical :: dangerous ! SECURITY ENHANCEMENT: Comprehensive shell metacharacter blocking ! Covers ALL possible command injection vectors across shells and encodings character(len=*), parameter :: DANGEROUS_CHARS = ';&|`$(){}[]<>*?!~"''^\#%@+=: ' dangerous = index(DANGEROUS_CHARS, char) > 0 ! Additional checks for control characters and Unicode exploits if (.not. dangerous) then ! Block control characters that could be used for injection dangerous = is_control_character(char) end if end function is_shell_injection_char !> Check if character is a control character function is_control_character(c) result(control) character, intent(in) :: c logical :: control integer :: char_code char_code = ichar(c) ! Control characters: 0-31 and 127 (DEL) control = (char_code >= CHAR_NULL .and. char_code <= CHAR_CTRL_END) .or. & (char_code == CHAR_DEL) end function is_control_character !> Validate path patterns for security function validate_path_patterns(path) result(valid) character(len=*), intent(in) :: path logical :: valid valid = .true. ! Check for directory traversal attacks if (index(path, '../') > 0 .or. index(path, '..\\') > 0) then call log_error("Security: Directory traversal detected in path: " // trim(path)) valid = .false. end if ! Check for double slashes if (index(path, '//') > 0) then call log_error("Security: Double slash detected in path: " // trim(path)) valid = .false. end if ! SECURITY ENHANCEMENT: Check for URL encoding attacks if (index(path, '%') > 0) then call log_error("Security: URL encoding detected (potential bypass attempt): " // trim(path)) valid = .false. end if ! Check for paths starting with slash (absolute paths) if (len_trim(path) > 0 .and. path(1:1) == '/') then call log_warning("Security: Absolute path detected: " // trim(path)) ! Note: Not invalid, just log warning end if end function validate_path_patterns !> Validate system paths function validate_system_paths(path) result(valid) character(len=*), intent(in) :: path logical :: valid valid = .true. ! Block access to sensitive system directories if (index(path, '/etc/') == 1 .or. index(path, '/sys/') == 1 .or. & index(path, '/proc/') == 1 .or. index(path, '/dev/') == 1) then call log_error("Security: Access to system directory blocked: " // trim(path)) valid = .false. end if end function validate_system_paths !> Get safe test output path function get_test_output_path(relative_path) result(full_path) character(len=*), intent(in) :: relative_path character(len=MAX_PATH_LENGTH) :: full_path character(len=MAX_PATH_LENGTH) :: sanitized_path character(len=*), parameter :: OUTPUT_DIR = "output/test/" ! Sanitize the input path sanitized_path = sanitize_filename(relative_path) ! Construct full path with test output directory if (len_trim(sanitized_path) > 0) then full_path = OUTPUT_DIR // trim(sanitized_path) else full_path = OUTPUT_DIR // "default_output" end if ! Validate the constructed path if (.not. is_safe_path(full_path)) then call log_error("Security: Generated unsafe path: " // trim(full_path)) full_path = OUTPUT_DIR // "safe_default_output" end if end function get_test_output_path !> Convert integer to string for logging function int_to_str(num) result(str) integer, intent(in) :: num character(len=20) :: str write(str, '(i0)') num end function int_to_str end module fortplot_security_core