Arbitrary Code Execution Vulnerability in Simple Form CVE-2019-16676

On 2019-08-02 we at bitcrowd discovered a security vulnerability in simple_form. simple_form is one of the go-to ways to easily handle HTML forms in Ruby on Rails. The security issue allows arbitrary code execution in the worst case, but at least has potential for data corruption or DOS attacks. This security issue was responsibly disclosed by bitcrowd. Find details about the timeline of the events and a more detailed description below.

DescriptionLink to section

On August 2nd 2019 developers at bitcrowd started a bug-fix ticket for one of its clients. The app in question is a CMS which offers its admins to configure custom key-value pair settings in the admin backend. The key of a setting was later used as the HTML <label> for a form implemented in simple_form. Thus, admins could control the input for the simple_form label method as shown in the following ERB template:

# Simplified version of the ERB template resulting in discovery of the security issue

<%= simple_form_for @important_record do |f| %>
  <%= f.label @user_supplied_string %>
  ...
  <% end %>
<% end %>

One of the admins added a setting with the key tap which resulted the later <label>-generation to throw a 500 server error. It seemed like the tap method was called with insufficient arguments.

During the inspection of that error, it turned out that calling f.label causes simple_form to executed a method with the given label name β€” tap in this case β€” on the forms object. This means that calling f.label 'delete' would result in deleting the @important_record in case it is an ActiveRecord object. In general, almost any method could be called on the forms object leading to a potential arbitrary code execution vulnerability. The exception are methods containing password, time_zone, country, email, phone, and url since these methods are handled differently as shown later in the code examples.

We did fix the issue with a work around for our client and deployed that to production. In addition we planned to responsibly report this issue to plataformatec, the author of simple_form.

For the report to be constructive, we attempted to find the cause of this bug. Since the cause is hidden behind only three methods, we outline our journey here. Please follow the # πŸ‘‰ comments, they will provide further context to the simple_form source code:

module SimpleForm
  class FormBuilder < ActionView::Helpers::FormBuilder

    # πŸ‘‰ The form builder is what we call in our ERB template within `simple_form_for`.
    # πŸ‘‰ In our vulnerable code above, we called `f.label @user_supplied_string`.
    # πŸ‘‰ This is the `label` method we called πŸ‘‡
    def label(attribute_name, *args)
      return super if args.first.is_a?(String) || block_given?

      options = args.extract_options!.dup
      options[:label_html] = options.except(:label, :label_text, :required, :as)

      column      = find_attribute_column(attribute_name)

      # πŸ‘‰ to generate the label HTML, simple_form creates the whole `<input>` object
      # πŸ‘‰ to just return the <label> part of it. To create a valid `<input>`, however,
      # πŸ‘‰ it must know which type the input should have. Since we didn't specify it
      # πŸ‘‰ e.g. with `f.label 'delete', as: :string` simple_form needs to guess.
      input_type  = default_input_type(attribute_name, column, options)
      SimpleForm::Inputs::Base.new(self, attribute_name, column, input_type, options).label
    end

    # ...

    def default_input_type(attribute_name, column, options)
      # πŸ‘‰ If you provide the `as:` option, it returns what was given with that option.
      # πŸ‘‰ This way the vulnerable code is never executed.
      # πŸ‘‰ However, in our case we didn't provide that option.
      return options[:as].to_sym if options[:as]
      custom_type = find_custom_type(attribute_name.to_s) and return custom_type
      return :select             if options[:collection]

      # πŸ‘‰ in the following case statement simple_form attempts to guess which type
      # πŸ‘‰ of <input> you want. This is done by checking some common name patterns.
      input_type = column.try(:type)
      case input_type
      when :timestamp
        :datetime
      when :string, :citext, nil
        case attribute_name.to_s
        when /(?:\b|\W|_)password(?:\b|\W|_)/  then :password
        when /(?:\b|\W|_)time_zone(?:\b|\W|_)/ then :time_zone
        when /(?:\b|\W|_)country(?:\b|\W|_)/   then :country
        when /(?:\b|\W|_)email(?:\b|\W|_)/     then :email
        when /(?:\b|\W|_)phone(?:\b|\W|_)/     then :tel
        when /(?:\b|\W|_)url(?:\b|\W|_)/       then :url
        else
          # πŸ‘‰ some names, like `delete` are not matched by the regular expressions above
          # πŸ‘‰ so it tries to guess if the given name is a method returning some kind of file
          file_method?(attribute_name) ? :file : (input_type || :string)
        end
      else
        input_type
      end
    end

    # ...

    def file_method?(attribute_name)
      # πŸ‘‰ the "is it a file?" check is done by calling the method and inspecting the result.
      # πŸ‘‰ This is where our given label-name is called as a method on the forms object πŸ”₯
      # πŸ‘‰ `@object.send(attribute_name)`
      file = @object.send(attribute_name) if @object.respond_to?(attribute_name)
      file && SimpleForm.file_methods.any? { |m| file.respond_to?(m) }
    end
  end
  end

Now that we had the cause of the bug, we could contact plataformatec about the issue at opensource@plataformatec.com.br. When reporting security vulnerabilities take care of how to report them. A good first source of how to behave can be found in the OWASP Vulnerability Disclosure Cheat Sheet. There is some danger in reporting security vulnerabilities the wrong way, or them being taken wrong by the author. So consider your options and consult a lawyer in case there are any uncertainties.

We decided to report the vulnerability responsibly to give plataformatec the needed time to release a fix for the issue. Plataformatec was very responsive on our first mail and provided us ongoing feedback about the state of the fix (without any lawyers involved ;)).

We can only thank them for open sourcing simple_form in the first place, and for handling security issues responsibly.

  • 2019-08-02: Discovered the issue and reported it to opensource@plataformatec.com.br
  • 2019-08-02: Answer from plataformatec, that they received the issue
  • 2019-08-09: Confirming the issue, they are working on a fix β€œfor next week”
  • 2019-09-19: Update from plataformatec including a source code diff with a potential fix. They said to request a CVE and prepare an announcement, and release the fix β€œnext week”
  • 2019-09-21: Bitcrowd proposed potential improvements on the fix
  • 2019-09-24: We found that a suggested code change would be too hard to maintain and that the security contact should be listed more prominently. Also both parties are preparing announcements on the issue.
  • 2019-09-27: Simple Form version 5.0 released, including the fix
Β© 2020 bitcrowd GmbH.