[prev in list] [next in list] [prev in thread] [next in thread] 

List:       oss-security
Subject:    [oss-security] Arbitrary file existence disclosure in Action Pack (CVE-2014-7818)
From:       Aaron Patterson <tenderlove () ruby-lang ! org>
Date:       2014-10-30 18:47:39
Message-ID: 20141030184739.GB71386 () TC ! local
[Download RAW message or body]

[Attachment #2 (multipart/mixed)]


Arbitrary file existence disclosure in Action Pack

There is an information leak vulnerability in Action Pack. This vulnerability
has been assigned the CVE identifier CVE-2014-7818.

Versions Affected:  >= 3.0.0
Not affected:       <= 3.0.0
Fixed Versions:     3.2.20, 4.0.11, 4.1.7, 4.2.0.beta3

Impact
------
Specially crafted requests can be used to determine whether a file exists on the filesystem \
that is outside the Rails application's root directory.  The files will not be served, but \
attackers can determine whether or not the file exists.

This only impacts Rails applications that enable static file serving at
runtime.  For example, the application's production configuration will say:

  config.serve_static_assets = true

All users running an affected release should either upgrade or use one of the work arounds \
immediately.

Releases 
-------- 
The 3.2.20, 4.0.11, 4.1.7 & 4.2.0.beta3 releases are available at the normal locations. 

Workarounds 
----------- 
To work around this issue, set config.serve_static_assets = false in an initializer.  This work \
around will not be possible in all hosting environments and upgrading is advised.

Patches 
------- 
To aid users who aren't able to upgrade immediately we have provided patches for the two \
supported release series.  They are in git-am format and consist of a single changeset. 

* 3-1-sec-static-files.patch - Patch for the 3.1.x release series
* 3-2-sec-static-files.patch - Patch for the 3.2.x release series
* 4-0-sec-static-files.patch - Patch for the 4.0.x release series
* 4-1-sec-static-files.patch - Patch for the 4.1.x release series

Please note that only the 3.2.x, 4.0.x & 4.1.x  series are supported at present.  Users of \
earlier unsupported releases are advised to upgrade as soon as possible as we cannot guarantee \
the continued availability of security fixes for unsupported releases.

Credits 
------- 

This vulnerability was reported by multiple researchers working independently.  Thanks to each \
of them for reporting the issue to us and verifying the fixes.

* Eaden McKee
* Dennis Hackethal & Christian Hansen of Crowdcurity
* Juan C. Müller & Mike McClurg of Greenhouse.io 
* Alex Ianus of Coinbase

-- 
Aaron Patterson
http://tenderlovemaking.com/


["3-1-sec-static-files.patch" (text/plain)]

From 9c37d8eb8821aa8dd5ac395ae85344a63ae0e23c Mon Sep 17 00:00:00 2001
From: Aaron Patterson <aaron.patterson@gmail.com>
Date: Fri, 10 Oct 2014 16:00:03 -0700
Subject: [PATCH] FileHandler should not be called for files outside the root

FileHandler#matches? should return false for files that are outside the
"root" path.

Conflicts:
	actionpack/lib/action_dispatch/middleware/static.rb

Conflicts:
	actionpack/lib/action_dispatch/middleware/static.rb
	actionpack/test/dispatch/static_test.rb
---
 actionpack/lib/action_dispatch/middleware/static.rb | 21 ++++++++++++++++++++-
 actionpack/test/dispatch/static_test.rb             | 18 ++++++++++++++++++
 2 files changed, 38 insertions(+), 1 deletion(-)

diff --git a/actionpack/lib/action_dispatch/middleware/static.rb \
b/actionpack/lib/action_dispatch/middleware/static.rb index 31185fe..4c46283 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -11,7 +11,7 @@ module ActionDispatch
     def match?(path)
       path = path.dup
 
-      full_path = path.empty? ? @root : File.join(@root, \
escape_glob_chars(unescape_path(path))) +      full_path = path.empty? ? @root : \
File.join(@root, escape_glob_chars(clean_path_info(unescape_path(path))))  paths = \
"#{full_path}#{ext}"  
       matches = Dir[paths]
@@ -40,6 +40,25 @@ module ActionDispatch
     def escape_glob_chars(path)
       path.gsub(/[*?{}\[\]]/, "\\\\\\&")
     end
+
+    private
+
+    PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
+
+    def clean_path_info(path_info)
+      parts = path_info.split PATH_SEPS
+
+      clean = []
+
+      parts.each do |part|
+        next if part.empty? || part == '.'
+        part == '..' ? clean.pop : clean << part
+      end
+
+      clean.unshift '/' if parts.empty? || parts.first.empty?
+
+      ::File.join(*clean)
+    end
   end
 
   class Static
diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb
index 949deef..f225b02 100644
--- a/actionpack/test/dispatch/static_test.rb
+++ b/actionpack/test/dispatch/static_test.rb
@@ -136,10 +136,28 @@ class StaticTest < ActiveSupport::TestCase
     [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]]
   }
   App = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, \
max-age=60") +  Root = "#{FIXTURE_LOAD_PATH}/public"
 
   def setup
     @app = App
+    @root = Root
   end
 
   include StaticTests
+
+  def test_custom_handler_called_when_file_is_outside_root
+    filename = 'shared.html.erb'
+    assert File.exist?(File.join(@root, '..', filename))
+    env = {
+      "REQUEST_METHOD"=>"GET",
+      "REQUEST_PATH"=>"/..%2F#{filename}",
+      "PATH_INFO"=>"/..%2F#{filename}",
+      "REQUEST_URI"=>"/..%2F#{filename}",
+      "HTTP_VERSION"=>"HTTP/1.1",
+      "SERVER_NAME"=>"localhost",
+      "SERVER_PORT"=>"8080",
+      "QUERY_STRING"=>""
+    }
+    assert_equal(DummyApp.call(nil), @app.call(env))
+  end
 end
-- 
1.9.3 (Apple Git-50)


["3-2-sec-static-files.patch" (text/plain)]

From 83c32fb543f72732509a943523f9c7a25a44c07b Mon Sep 17 00:00:00 2001
From: Aaron Patterson <aaron.patterson@gmail.com>
Date: Fri, 10 Oct 2014 16:00:03 -0700
Subject: [PATCH] FileHandler should not be called for files outside the root

FileHandler#matches? should return false for files that are outside the
"root" path.

Conflicts:
	actionpack/lib/action_dispatch/middleware/static.rb

Conflicts:
	actionpack/lib/action_dispatch/middleware/static.rb
	actionpack/test/dispatch/static_test.rb
---
 actionpack/lib/action_dispatch/middleware/static.rb | 21 ++++++++++++++++++++-
 actionpack/test/dispatch/static_test.rb             | 18 ++++++++++++++++++
 2 files changed, 38 insertions(+), 1 deletion(-)

diff --git a/actionpack/lib/action_dispatch/middleware/static.rb \
b/actionpack/lib/action_dispatch/middleware/static.rb index a8d1765..7f11170 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -12,7 +12,7 @@ module ActionDispatch
     def match?(path)
       path = path.dup
 
-      full_path = path.empty? ? @root : File.join(@root, \
escape_glob_chars(unescape_path(path))) +      full_path = path.empty? ? @root : \
File.join(@root, escape_glob_chars(clean_path_info(unescape_path(path))))  paths = \
"#{full_path}#{ext}"  
       matches = Dir[paths]
@@ -42,6 +42,25 @@ module ActionDispatch
       path.force_encoding('binary') if path.respond_to? :force_encoding
       path.gsub(/[*?{}\[\]]/, "\\\\\\&")
     end
+
+    private
+
+    PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
+
+    def clean_path_info(path_info)
+      parts = path_info.split PATH_SEPS
+
+      clean = []
+
+      parts.each do |part|
+        next if part.empty? || part == '.'
+        part == '..' ? clean.pop : clean << part
+      end
+
+      clean.unshift '/' if parts.empty? || parts.first.empty?
+
+      ::File.join(*clean)
+    end
   end
 
   class Static
diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb
index 856746c..a035abd 100644
--- a/actionpack/test/dispatch/static_test.rb
+++ b/actionpack/test/dispatch/static_test.rb
@@ -140,10 +140,28 @@ class StaticTest < ActiveSupport::TestCase
     [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]]
   }
   App = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, \
max-age=60") +  Root = "#{FIXTURE_LOAD_PATH}/public"
 
   def setup
     @app = App
+    @root = Root
   end
 
   include StaticTests
+
+  def test_custom_handler_called_when_file_is_outside_root
+    filename = 'shared.html.erb'
+    assert File.exist?(File.join(@root, '..', filename))
+    env = {
+      "REQUEST_METHOD"=>"GET",
+      "REQUEST_PATH"=>"/..%2F#{filename}",
+      "PATH_INFO"=>"/..%2F#{filename}",
+      "REQUEST_URI"=>"/..%2F#{filename}",
+      "HTTP_VERSION"=>"HTTP/1.1",
+      "SERVER_NAME"=>"localhost",
+      "SERVER_PORT"=>"8080",
+      "QUERY_STRING"=>""
+    }
+    assert_equal(DummyApp.call(nil), @app.call(env))
+  end
 end
-- 
1.9.3 (Apple Git-50)


["4-0-sec-static-files.patch" (text/plain)]

From c47a4ae57b958c8e5b79d2b4636409f54d046d96 Mon Sep 17 00:00:00 2001
From: Aaron Patterson <aaron.patterson@gmail.com>
Date: Fri, 10 Oct 2014 16:00:03 -0700
Subject: [PATCH] FileHandler should not be called for files outside the root

FileHandler#matches? should return false for files that are outside the
"root" path.

Conflicts:
	actionpack/lib/action_dispatch/middleware/static.rb
---
 .../lib/action_dispatch/middleware/static.rb       | 22 +++++++++++++++++++++-
 actionpack/test/dispatch/static_test.rb            | 22 ++++++++++++++++++++--
 2 files changed, 41 insertions(+), 3 deletions(-)

diff --git a/actionpack/lib/action_dispatch/middleware/static.rb \
b/actionpack/lib/action_dispatch/middleware/static.rb index 2764584..df77021 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -14,7 +14,8 @@ module ActionDispatch
       path = unescape_path(path)
       return false unless path.valid_encoding?
 
-      full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(path))
+      full_path = path.empty? ? @root : File.join(@root,
+        clean_path_info(escape_glob_chars(path)))
       paths = "#{full_path}#{ext}"
 
       matches = Dir[paths]
@@ -43,6 +44,25 @@ module ActionDispatch
     def escape_glob_chars(path)
       path.gsub(/[*?{}\[\]]/, "\\\\\\&")
     end
+
+    private
+
+    PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
+
+    def clean_path_info(path_info)
+      parts = path_info.split PATH_SEPS
+
+      clean = []
+
+      parts.each do |part|
+        next if part.empty? || part == '.'
+        part == '..' ? clean.pop : clean << part
+      end
+
+      clean.unshift '/' if parts.empty? || parts.first.empty?
+
+      ::File.join(*clean)
+    end
   end
 
   class Static
diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb
index d3fe9df..79a7975 100644
--- a/actionpack/test/dispatch/static_test.rb
+++ b/actionpack/test/dispatch/static_test.rb
@@ -147,7 +147,8 @@ class StaticTest < ActiveSupport::TestCase
   }
 
   def setup
-    @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, \
max-age=60") +    @root = "#{FIXTURE_LOAD_PATH}/public"
+    @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60")
   end
 
   def public_path
@@ -155,11 +156,28 @@ class StaticTest < ActiveSupport::TestCase
   end
 
   include StaticTests
+
+  def test_custom_handler_called_when_file_is_outside_root
+    filename = 'shared.html.erb'
+    assert File.exist?(File.join(@root, '..', filename))
+    env = {
+      "REQUEST_METHOD"=>"GET",
+      "REQUEST_PATH"=>"/..%2F#{filename}",
+      "PATH_INFO"=>"/..%2F#{filename}",
+      "REQUEST_URI"=>"/..%2F#{filename}",
+      "HTTP_VERSION"=>"HTTP/1.1",
+      "SERVER_NAME"=>"localhost",
+      "SERVER_PORT"=>"8080",
+      "QUERY_STRING"=>""
+    }
+    assert_equal(DummyApp.call(nil), @app.call(env))
+  end
 end
 
 class StaticEncodingTest < StaticTest
   def setup
-    @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/公共", "public, \
max-age=60") +    @root = "#{FIXTURE_LOAD_PATH}/公共"
+    @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60")
   end
 
   def public_path
-- 
1.9.3 (Apple Git-50)


["4-1-sec-static-files.patch" (text/plain)]

From 22ec7261be083ffaf8920b4c10e1e0504e26e186 Mon Sep 17 00:00:00 2001
From: Aaron Patterson <aaron.patterson@gmail.com>
Date: Fri, 10 Oct 2014 16:00:03 -0700
Subject: [PATCH] FileHandler should not be called for files outside the root

FileHandler#matches? should return false for files that are outside the
"root" path.

Conflicts:
	actionpack/lib/action_dispatch/middleware/static.rb
---
 .../lib/action_dispatch/middleware/static.rb       | 22 +++++++++++++++++++++-
 actionpack/test/dispatch/static_test.rb            | 22 ++++++++++++++++++++--
 2 files changed, 41 insertions(+), 3 deletions(-)

diff --git a/actionpack/lib/action_dispatch/middleware/static.rb \
b/actionpack/lib/action_dispatch/middleware/static.rb index 2764584..df77021 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -14,7 +14,8 @@ module ActionDispatch
       path = unescape_path(path)
       return false unless path.valid_encoding?
 
-      full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(path))
+      full_path = path.empty? ? @root : File.join(@root,
+        clean_path_info(escape_glob_chars(path)))
       paths = "#{full_path}#{ext}"
 
       matches = Dir[paths]
@@ -43,6 +44,25 @@ module ActionDispatch
     def escape_glob_chars(path)
       path.gsub(/[*?{}\[\]]/, "\\\\\\&")
     end
+
+    private
+
+    PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
+
+    def clean_path_info(path_info)
+      parts = path_info.split PATH_SEPS
+
+      clean = []
+
+      parts.each do |part|
+        next if part.empty? || part == '.'
+        part == '..' ? clean.pop : clean << part
+      end
+
+      clean.unshift '/' if parts.empty? || parts.first.empty?
+
+      ::File.join(*clean)
+    end
   end
 
   class Static
diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb
index afdda70..4e9cdc3 100644
--- a/actionpack/test/dispatch/static_test.rb
+++ b/actionpack/test/dispatch/static_test.rb
@@ -154,7 +154,8 @@ class StaticTest < ActiveSupport::TestCase
   }
 
   def setup
-    @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, \
max-age=60") +    @root = "#{FIXTURE_LOAD_PATH}/public"
+    @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60")
   end
 
   def public_path
@@ -162,11 +163,28 @@ class StaticTest < ActiveSupport::TestCase
   end
 
   include StaticTests
+
+  def test_custom_handler_called_when_file_is_outside_root
+    filename = 'shared.html.erb'
+    assert File.exist?(File.join(@root, '..', filename))
+    env = {
+      "REQUEST_METHOD"=>"GET",
+      "REQUEST_PATH"=>"/..%2F#{filename}",
+      "PATH_INFO"=>"/..%2F#{filename}",
+      "REQUEST_URI"=>"/..%2F#{filename}",
+      "HTTP_VERSION"=>"HTTP/1.1",
+      "SERVER_NAME"=>"localhost",
+      "SERVER_PORT"=>"8080",
+      "QUERY_STRING"=>""
+    }
+    assert_equal(DummyApp.call(nil), @app.call(env))
+  end
 end
 
 class StaticEncodingTest < StaticTest
   def setup
-    @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/公共", "public, \
max-age=60") +    @root = "#{FIXTURE_LOAD_PATH}/公共"
+    @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60")
   end
 
   def public_path
-- 
1.9.3 (Apple Git-50)


[Attachment #9 (application/pgp-signature)]

[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic