noizZze

Paperclip Processors Vulnerability

Recently, the project I was working on was deployed for public testing and one particularly interesting vulnerability was discovered that let the attackers execute arbitrary code on the server with the application owner permissions. I would like to share this information for you to avoid the same trap.

In this project we use Paperclip and needed a custom processor to perform the encryption of the uploaded files. Paperclip has a very convenient interface for processors that you can implement and then use to alter the uploaded files – perform OCR, rotate images, archive data, compress JS / CSS and do a million of other useful things. When the file is uploaded, Paperclip saves it in a temporary location (usually /tmp) and lets all configured processors to work their magic on that file. Later, when and if the processing succeeded, the file is moved to a permanent location and is given the correct name.

The danger lies in the naming of the temporary file. Here’s how the temporary file is named:

1
2
3
4
5
6
7
class Tempfile < ::Tempfile
  # Replaces Tempfile's +make_tmpname+ with one that honors file extensions.
  def make_tmpname(basename, n)
    extension = File.extname(basename)
    sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n.to_i, extension)
  end
end

Paperclip, when saving the uploaded file, does it like this:

1
tempfile = Paperclip::Tempfile.new("stream" + File.extname(name))

(where the “name” is the name of the originally uploaded file – the one that is the name of the file you choose to upload)

The resulting temporary file that is created has the format “stream,PID,N.EXTENSION”, where “N” is counting from 0 for each new file and “EXTENSION” is the original extension of your file. We are getting close, bear with me.

Now lets assume that you want to process the uploaded file in a way and you have the command-line tool for that named (surprisingly) “processor”, and so you write the following processor code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module Paperclip
  class Encrypt < Processor
    def initialize(file, options = {}, attachment = nil)
      super

      @file           = file
      @recipient      = options[:geometry]
      @attachment     = attachment
      @current_format = File.extname(@file.path)
      @basename       = File.basename(@file.path, @current_format)
    end

    def make
      src = @file

      begin
        Paperclip.run("processor", "\"#{File.expand_path(src.path)}\"")
      rescue PaperclipCommandLineError
        raise PaperclipError, "couldn't be processed. Please try again later."
      end
    end
  end
end

Here’s where it all happened. In the line where we run the command-line tool, we give the name of the file as an argument, and that’s all fine for the name like “image.png”, but gets completely out of hand when someone sends the file with the name like “image.$( sleep 10 )”. In this later case, the temp file created by Paperclip and passed to your processor will look like this:

1
/tmp/stream,12345,0.$( sleep 10 )

… and when used in the command line, like this:

1
process "/tmp/stream,12345,0.$( sleep 10 )"

It will put the server in deep coma for 10 seconds. Now imagine if it’s not “sleep 10”, but “rm -rf ~” or anything else copying important stuff to publicly accessible places?

So what’s the cure? My solution was to check extension of the temp file and disallow anything that’s not what we want to accept. It’s simple and simple is what we need here.

Please let me know if you have a better way of solving this or found something I missed.