Source Browser

class SourceBrowser
class NotAllowed < StandardError; end
class << self
def list(relative_dir = "")
validate_path!(relative_dir) if relative_dir.present?
allowed = whitelisted_files
prefix = relative_dir.present? ? "#{relative_dir}/" : ""
entries = Set.new
allowed.each do |file|
next unless file.start_with?(prefix)
remainder = file.delete_prefix(prefix)
top = remainder.split("/").first
full = prefix + top
is_dir = remainder.include?("/")
entries << { name: top, path: full, directory: is_dir }
end
entries.sort_by { |e| [e[:directory] ? 0 : 1, e[:name]] }
end
def read(relative_path)
validate_path!(relative_path)
unless whitelisted_files.include?(relative_path)
raise NotAllowed, "File not in whitelist"
end
full_path = Rails.root.join(relative_path)
raise NotAllowed, "File not found" unless File.file?(full_path)
File.read(full_path)
end
def file?(relative_path)
whitelisted_files.include?(relative_path)
end
def language_for(path)
case File.extname(path)
when ".rb" then "ruby"
when ".erb" then "erb"
when ".yml", ".yaml" then "yaml"
when ".js" then "javascript"
when ".css" then "css"
else "plaintext"
end
end
private
def validate_path!(path)
raise NotAllowed, "Invalid path" if path.nil?
raise NotAllowed, "Null bytes not allowed" if path.include?("\0")
raise NotAllowed, "Invalid path" if path.include?("..")
expanded = File.expand_path(path, Rails.root)
root = Rails.root.to_s
unless expanded.start_with?("#{root}/")
raise NotAllowed, "Path traversal detected"
end
end
def whitelisted_files
@whitelisted_files ||= compute_whitelist
end
def compute_whitelist
globs = Rails.application.config.source_browser_whitelist
root = Rails.root.to_s
files = Set.new
globs.each do |glob|
Dir[Rails.root.join(glob)].each do |full_path|
next unless File.file?(full_path)
files << full_path.delete_prefix("#{root}/")
end
end
files
end
end
# Reload whitelist cache in development
def self.reload!
@whitelisted_files = nil
end
end

← Back