Develop the right mindset for Rails security
Avoid shipping vulnerable code by learning how to prevent security issues in your Rails applications.
Get the course for $99RubyLLM 2 is in the final stages of being released and when I learned about this, I went to check the documentation to see if there is anything new that’s interesting to me. While browsing the docs, I came across this example:
## Attachments and Structured Output
# With Active Storage on your message model, files passed to `ask` are persisted with the user message:
```ruby
uploaded_file = params[:uploaded_file]
chat.ask("What is in this file?", with: params[:uploaded_file])
This is an innocent looking example at first sight, but if I see any user supplied value, I am always curious how is that handled and if it can be abused to trigger a vulnerability. The example says that if your model accepts an Active Storage upload, then this param will be the file and that’s what will be passed around. But since this is just a request param, it can be actually set to anything. So I created a little test app and poked at this example with various data. First, I tried a URL to see what happens.
curl --data-urlencode "uploaded_file=http://localhost:9292/?hi"
I had a listener on port 9292 and I could see that there is a request made to it. This means that a malicious user, could use this for a Server-Side Request forgery, because under the hood, RubyLLM::Attachment makes an HTTP request if the attachment is a URL. While looking at this Attachment class, I noticed there is the following method:
def load_content_from_path
@content = File.binread(@source)
end
So I tried to send a local file path to my little example app:
curl --data-urlencode "uploaded_file=./config/database.yml"
This triggered a file not found error in the Rails app and that means we could use this for Local File Inclusion and in case the integration reflects anything back to the user about the attachment, a malicious user could use this to access local files the process has access to.
After verifying these, I sent the details to Carmine, the maintainer of RubyLLM and since this is intended functionality, he updated the examples to make sure nobody ends up with a vulnerable implementation based on it. These is a safe example:
uploaded_file = params[:uploaded_file]
return head :bad_request unless uploaded_file.is_a?(ActionDispatch::Http::UploadedFile)
chat.ask("What is in this file?", with: uploaded_file)
The moral of the story is the ancient advise: never trust user input. And this example also demonstrates how easy it is to miss a potential issue with some innocent looking Ruby code.