CVE-2019-18978 (published 2019-11-14T21:15Z)

An issue was discovered in the rack-cors (aka Rack CORS Middleware) gem before 1.0.4 for Ruby. It allows ../ directory traversal to access private resources because resource matching does not ensure that pathnames are in a canonical format.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/cyu/rack-cors
commit_details: repo_url: https://github.com/cyu/rack-cors
author: Calvin Yu
author_date: 2019-11-13 22:55:41-05:00
msg: Unescape and resolve paths before resource checks

Fix scenario where someone could use '../' to access private resources
repository: repo_url: https://github.com/cyu/rack-cors
repo_name: cyu/rack-cors
description: Rack Middleware for handling Cross-Origin Resource Sharing (CORS), which makes cross-origin AJAX possible.
repo_language: JavaScript
owner: cyu
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,10 @@
 # Change Log
 All notable changes to this project will be documented in this file.
 
+## 1.0.4 - 2019-11-13
+### Security
+- Escape and resolve path before evaluating resource rules (thanks to Colby Morgan)
+
 ## 1.0.3 - 2019-03-24
 ### Changed
 - Don't send 'Content-Type' header with pre-flight requests

programming_language: Markdown
filename: cors.rb
change_type: ModificationType.MODIFY
diff: @@ -64,24 +64,27 @@ module Rack
     def call(env)
       env[HTTP_ORIGIN] ||= env[HTTP_X_ORIGIN] if env[HTTP_X_ORIGIN]
 
+      path = evaluate_path(env)
+
       add_headers = nil
       if env[HTTP_ORIGIN]
         debug(env) do
           [ 'Incoming Headers:',
             "  Origin: #{env[HTTP_ORIGIN]}",
+            "  Path-Info: #{path}",
             "  Access-Control-Request-Method: #{env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]}",
             "  Access-Control-Request-Headers: #{env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]}"
             ].join("\n")
         end
         if env[REQUEST_METHOD] == OPTIONS and env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
-          headers = process_preflight(env)
+          headers = process_preflight(env, path)
           debug(env) do
             "Preflight Headers:\n" +
                 headers.collect{|kv| "  #{kv.join(': ')}"}.join("\n")
           end
           return [200, headers, []]
         else
-          add_headers = process_cors(env)
+          add_headers = process_cors(env, path)
         end
       else
         Result.miss(env, Result::MISS_NO_ORIGIN)
@@ -90,7 +93,7 @@ module Rack
       # This call must be done BEFORE calling the app because for some reason
       # env[PATH_INFO] gets changed after that and it won't match. (At least
       # in rails 4.1.6)
-      vary_resource = resource_for_path(env[PATH_INFO])
+      vary_resource = resource_for_path(path)
 
       status, headers, body = @app.call env
 
@@ -147,14 +150,20 @@ module Rack
         end
       end
 
+      def evaluate_path(env)
+        path = env[PATH_INFO]
+        path = Rack::Utils.clean_path_info(Rack::Utils.unescape_path(path)) if path
+        path
+      end
+
       def all_resources
         @all_resources ||= []
       end
 
-      def process_preflight(env)
+      def process_preflight(env, path)
         result = Result.preflight(env)
 
-        resource, error = match_resource(env)
+        resource, error = match_resource(path, env)
         unless resource
           result.miss(error)
           return {}
@@ -163,8 +172,8 @@ module Rack
         return resource.process_preflight(env, result)
       end
 
-      def process_cors(env)
-        resource, error = match_resource(env)
+      def process_cors(env, path)
+        resource, error = match_resource(path, env)
         if resource
           Result.hit(env)
           cors = resource.to_headers(env)
@@ -185,8 +194,7 @@ module Rack
         nil
       end
 
-      def match_resource(env)
-        path   = env[PATH_INFO]
+      def match_resource(path, env)
         origin = env[HTTP_ORIGIN]
 
         origin_matched = false

programming_language: Ruby
filename: version.rb
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 module Rack
   class Cors
-    VERSION = "1.0.3"
+    VERSION = "1.0.4"
   end
 end

programming_language: Ruby
filename: cors_test.rb
change_type: ModificationType.MODIFY
diff: @@ -146,6 +146,12 @@ describe Rack::Cors do
     last_response.headers['Vary'].must_equal 'Origin, Host'
   end
 
+  it "decode URL and resolve paths before resource matching" do
+    header 'Origin', 'http://localhost:3000'
+    get '/public/a/..%2F..%2Fprivate/stuff'
+    last_response.wont_render_cors_success
+  end
+
   describe 'with array of upstream Vary headers' do
     let(:app) { load_app('test', { proxy: true }) }
 

programming_language: Ruby
filename: test.ru
change_type: ModificationType.MODIFY
diff: @@ -41,6 +41,7 @@ use Rack::Cors do
   allow do
     origins '*'
     resource '/public'
+    resource '/public/*'
     resource '/public_without_credentials', :credentials => false
   end
 

programming_language: Ruby