Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/tui.rb
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,28 @@ def emoji(char)
end
end

# Security module for sanitizing user input to prevent command injection
module Security
# Shell metacharacters that could be used for command injection
SHELL_METACHARACTERS = /[;|&$`<>(){}[\]\\!#*?~\n\r]/

module_function

# Sanitize user input to remove shell metacharacters
# This prevents command injection attacks when user input might be used in shell contexts
def sanitize_input(input)
return "" if input.nil?
# Remove all shell metacharacters to prevent command injection
input.to_s.gsub(SHELL_METACHARACTERS, '')
end

# Check if input contains potentially dangerous shell metacharacters
def contains_shell_metacharacters?(input)
return false if input.nil?
!!(input.to_s =~ SHELL_METACHARACTERS)
end
end

class Terminal
class << self
def size(io = $stderr)
Expand Down
25 changes: 23 additions & 2 deletions try.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
class TrySelector
include Tui::Helpers
TRY_PATH = ENV['TRY_PATH'] || File.expand_path("~/src/tries")
MAX_INPUT_LENGTH = 1024 # Maximum input length to prevent DoS attacks

def initialize(search_term = "", base_path: TRY_PATH, initial_input: nil, test_render_once: false, test_no_cls: false, test_keys: nil, test_confirm: nil)
@search_term = search_term.gsub(/\s+/, '-')
Expand All @@ -19,7 +20,7 @@ def initialize(search_term = "", base_path: TRY_PATH, initial_input: nil, test_r
@input_cursor_pos = @input_buffer.length # Start at end of buffer
@selected = nil
@all_trials = nil # Memoized trials
@base_path = base_path
@base_path = File.realpath(base_path) # Resolve to absolute path to prevent traversal
@delete_status = nil # Status message for deletions
@delete_mode = false # Whether we're in deletion mode
@marked_for_deletion = [] # Paths marked for deletion
Expand All @@ -34,6 +35,21 @@ def initialize(search_term = "", base_path: TRY_PATH, initial_input: nil, test_r
FileUtils.mkdir_p(@base_path) unless Dir.exist?(@base_path)
end

# Security: Validate that a path is within the base directory (prevent path traversal)
def path_within_base?(path)
real_path = File.realpath(path) rescue nil
return false unless real_path
real_path.start_with?(@base_path + File::SEPARATOR) || real_path == @base_path
end

# Security: Log security-sensitive events
def security_log(event, details = {})
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
log_entry = "[#{timestamp}] #{event}: #{details.inspect}\n"
log_file = File.join(@base_path, '.security.log')
File.open(log_file, 'a') { |f| f.write(log_entry) } rescue nil
end

def run
# Always use STDERR for rendering (it stays connected to TTY)
# This allows stdout to be captured for the shell commands
Expand Down Expand Up @@ -98,6 +114,10 @@ def load_all_tries
next if entry.start_with?('.')

path = File.join(@base_path, entry)

# Security: Validate path is within base directory (prevent path traversal)
next unless path_within_base?(path)

stat = File.stat(path)

# Only include directories
Expand Down Expand Up @@ -265,7 +285,8 @@ def main_loop
end
when String
# Only accept printable characters, not escape sequences
if key.length == 1 && key =~ /[a-zA-Z0-9\-\_\. ]/
# Security: Enforce maximum input length to prevent DoS
if key.length == 1 && key =~ /[a-zA-Z0-9\-\_\. ]/ && @input_buffer.length < MAX_INPUT_LENGTH
@input_buffer = @input_buffer[0...@input_cursor_pos] + key + @input_buffer[@input_cursor_pos..-1]
@input_cursor_pos += 1
@cursor_pos = 0 # Reset list selection when typing
Expand Down