Sửa lỗi bảo mật File Access Vulnerability trong Ruby on Rails

Lỗi File Access Vulnerability là gì?

Lỗi File Access Vulnerability là một lỗi bảo mật cho phép kẻ tấn công có thể sử dụng các lời gọi để thực hiện thêm, sửa hoặc xóa file trên server hoặc trên hệ thống file mà app đang sử dụng (ví dụ như S3) mà đáng ra họ không có quyền xử lý đến. Dưới đây là một ví dụ về một lời gọi có thể cho phép kẻ tấn công có thể kết nối đến file dữ liệu trong thư mục public của server Rails:

# http://domain.com?payload=config/database.yml
payload = params[:payload]

path = Rails.root.join(payload)
id = SecureRandom.uuid

File.link(path, "public/#{id}")

redirect_to "/#{id}"

Mặc dù trông không có gì khả nghi, nhưng đoạn code trên là một ví dụ hoàn hảo cho kiểu tấn công File Access. Kẻ tấn công có thể lợi dụng đoạn code này để kết nối đến một file mà ta không muốn để lộ ra.

Vấn đề khó khăn ở đây là có một số lượng lớn các method có thể bị lợi dụng để thực hiện tấn công File Access. Dựa trên source code của brakeman, ta có thể liệt kê một danh sách các method nguy hiểm sau:

# Dir:
Dir[]
Dir.chdir
Dir.chroot
Dir.delete
Dir.entries
Dir.foreach
Dir.glob
Dir.new
Dir.open
Dir.rmdir
Dir.unlink

# File
File.delete
File.foreach
File.lchmod
File.lchown
File.link
File.new
File.open
File.read
File.readlines
File.rename
File.symlink
File.sysopen
File.truncate
File.unlink

# FileUtils
FileUtils.cd
FileUtils.chdir
FileUtils.chmod
FileUtils.chmod_R
FileUtils.chown
FileUtils.chown_R
FileUtils.cmp
FileUtils.compare_file
FileUtils.compare_stream
FileUtils.copy
FileUtils.copy_entry
FileUtils.copy_file
FileUtils.copy_stream
FileUtils.cp
FileUtils.cp_r
FileUtils.getwd
FileUtils.install
FileUtils.link
FileUtils.ln
FileUtils.ln_s
FileUtils.ln_sf
FileUtils.makedirs
FileUtils.mkdir
FileUtils.mkdir_p
FileUtils.mkpath
FileUtils.move
FileUtils.mv
FileUtils.pwd
FileUtils.remove
FileUtils.remove_dir
FileUtils.remove_entry
FileUtils.remove_entry_secure
FileUtils.remove_file
FileUtils.rm
FileUtils.rm_f
FileUtils.rm_r
FileUtils.rm_rf
FileUtils.rmdir
FileUtils.rmtree
FileUtils.safe_unlink
FileUtils.symlink
FileUtils.touch

# IO
IO.foreach
IO.new
IO.open
IO.read
IO.readlines
IO.sysopen

# Kernel
Kernel.load
Kernel.open
Kernel.readlines

# Net::FTP
Net::FTP.new
Net::FTP.open

# Net::HTTP
Net::HTTP.new

# PStore
PStore.new

# Pathname
Pathname.glob
Pathname.new

# Shell
Shell.new

# YAML
YAML.load_file
YAML.parse_file

Sử dụng một method trong danh sách dài đến đáng sợ phía trên cho user input cũng đông nghĩa với việc mở ra một lỗ hổng để tấn công vào hệ thống.

Để dễ dàng theo dõi, ta phân loại các nhóm lệnh dựa trên khả năng tấn công của chúng vào hệ thống. Và độ nguy hiểm của từng loại tấn công cũng khác nhau:

STT Phương thức tấn công Tên method
1 Làm đầy dung lượng FileUtils.copy, FileUtils.cp, File.new, IO.new, PStore.new
2 Di chuyển file đến một địa chỉ cho phép download File.rename, FileUtils.move
3 Kết nối file đến một link cho phép download File.link, File.symlink, FileUtils.link, FileUtils.ln
4 Xóa file (DoS) Dir.delete, FileUtils.rm
5 Thay đổi quyền truy cập file (DoS) File.chmod, File.chown, FileUtils.chmod, FileUtils.chown
6 Đổi tên file File.rename, FileUtils.move, FileUtils.mv
7 Làm lộ file path FileUtils.pwd
8 Tải file lạ vào server Net::FTP.new, Net::HTTP.new
9 Tấn công website khác Net::FTP.new, Net::HTTP.new

Làm sao đến sửa lỗi File Access Vulnerability?

Cách đúng nhất, hiệu quả nhất để chống lại File Access Vulnerability là không cho nó xuất hiện ngay từ đầu và hạn chế mọi hoạt động không cần thiết ở mức độ hệ thống.

Hết bài! Bạn có thể ngừng đọc ở đây!

Nếu vẫn tiếp tục đọc thì có thể bạn đã nhận ra rằng lời khuyên phía trên mặc dù rất đúng, nhưng nó hoàn toàn vô nghĩa và vô dụng trong các trường hợp thực tế. Thay vào đó, ta phải áp dụng các kỹ thuật khác vào để chống lại tấn công File Access trong khi làm việc với các file trong hệ thống.

Sử dụng Identifier

Cách đầu tiên có thể sử dụng là dùng một Identifier để trỏ đến một file trong ổ đĩa. Việc này chỉ đơn giản là cấp cho file một ID, hash hoặc GUID.

# HTML
<select name="file_guid">
  <option value="690e1597-de8d-4912-ac04-d0e626f806f4">file1.log</option>
  <option value="2e157fa3-ea1e-4b46-931e-c0f8b10bfcb2">file2.log</option>
  <option value="fffb938b-07bc-472c-a48f-383123a9f04d">file3.log</option>
</select>

# Controller
download = FileDownload.find_by(file_guid: params[:file_guid])
send_file(download.path, filename: download.name, type: "text/plain")

Ở đoạn code trên ta gửi lên server một mã GUID, chứ không phải là tên file thật. Điều này sẽ khiến kẻ tấn công không thể tải file không được phép, và không sợ thao tác thay đổi tên file hay đường dẫn. Kỹ thuật này có thể dùng tốt trong việc di chuyển, sửa, đổi tên, xóa và gửi file trong trường hợp biết trước được tên file và đường dẫn. Đây là cách tốt nhất để bảo vệ hệ thống.

Hạn chế một phần

Về lý tưởng thì ta không cần đến bất cứ kỹ thuật nào khác để bảo vệ ngoài Identifier, nhưng đời không như là mơ. Đôi khi ta không có đủ thông tin cần thiết để sử dụng Identifier, trong trường hợp này ta vẫn phải cho user một sự tự do nhất định và hạn chế tối đa quyền truy cập vào các file hệ thống:

# HTML
<select name="file_name">
  <option value="file1.log">file1.log</option>
  <option value="file2.log">file2.log</option>
  <option value="file3.log">file3.log</option>
</select>

# Controller
file_name = sanitize(params[:file_name])

# if possible current_user.download_directory should be an identifier
# and controlled 100% by the server.
download_path = "downloads/#{current_user.download_directory}/#{file_name}"

if File.exists?(download_path)
  send_file download_path, filename: file_name, type: "text/plain"
else
  # return an error message
end

Ở đây ta có thể dùng thêm hàm sanitize để "làm sạch" param[:file_name] khỏi những ký tự nguy hiểm. Bằng cách này ta có thể kiểm soát được các truy cập vào file hệ thống.

Sử dụng filter

Kỹ thuật tiếp theo để hạn chế lỗi File Access là chỉ chấp nhận một số loại file nhất định. Ta phải tạo một "whitelist" gồm các kiểu file mà user được phép truy cập, ví dụ như chỉ chấp nhận file .pdf ở ví dụ dưới đây:

payload = sanitize(params[:filename])
if payload =~ /.pdf$/
  send_file("downloads/#{payload}", filename: 'report.pdf', type: "application/pdf")
else
  raise "Unknown file format requested"
end

Hàng phòng ngự này giúp ta không bị lộ ra những thông tin nhạy cảm như file database.yml. Và tất nhiên là ta phải sử dụng cùng với hàm sanitize để bảo vệ.

Việc giới hạn các kiểu file có thể bị vượt qua nếu kẻ tấn công có khả năng di chuyển hoặc đổi tên file. Ví dụ như họ có thể thêm một phần mở rộng .pdf vào file database.yml thì họ có thể tải về file database.yml.pdf mà không gặp cản trở gì.

Lưu file của user ở một server khác

Vào thời đại này thì dung lượng lưu trữ rất rẻ. Một cách khá tốt để bảo vệ hệ thống là giới hạn những dữ liệu được quyền lưu trên server. Thay vào đó ta sử dụng những công cụ như Amazon S3, DreamHost’s Dream Objects để lưu trữ file, tạo report... trên server bên ngoài được kết nối đến server chính.

Tất nhiên sử dụng phương pháp này bạn vẫn có thể tự bắn vào chân mình và trao quyền truy cập cho kẻ tấn công vào những file được lưu trữ bên ngoài. Việc lưu trữ file bên ngoài chỉ đơn giản là tạo ra một ranh giới giữa các file của hệ thống và của user, nhờ vậy mà việc tác động vào các file trong lưu trữ không ảnh hưởng đến web server.

Điều này có lợi ích thêm là việc này giúp giảm tải cho application server mà cắt bớt các thủ tục xử lý file.

Hạn chế sử dụng các method nguy hiểm

Có một tỉ lệ không nhỏ là những kỹ thuật kể trên, và cả những kỹ thuật thần thánh mà bạn tìm được đâu đó trên mạng, không thể áp dụng được vào trường hợp của bạn. Vào những lúc đó thì ta buộc phải quay lại với lời khuyên hàng đầu là không sử dụng những method nguy hiểm để bảo vệ cho app.

Khi nhận được yêu cầu làm một feature liên quan đến file access, bạn có thể feedback bằng một bản báo cáo đầy đủ những lỗi tiềm tàng mà feature mới đó có thể đem lại cho hệ thống.