Added more tests and more aggressive input checking.
[arvados.git] / services / api / app / models / commit.rb
index c371ac278bb0f0faa7ccf5e6f4769705ec274bd7..49541248f28cf5b4f677710aa30f03991a2fa6af 100644 (file)
@@ -1,18 +1,26 @@
 class Commit < ActiveRecord::Base
   require 'shellwords'
 
+  def self.git_check_ref_format(e)
+    if !e or e.empty? or e[0] == '-' or e[0] == '$'
+      # definitely not valid
+      false
+    else
+      `git check-ref-format --allow-onelevel #{e.shellescape}`
+      $?.success?
+    end
+  end
+
   def self.find_commit_range(current_user, repository, minimum, maximum, exclude)
-    # disallow starting with '-' so verision strings can't be interpreted as command line options
-    valid_pattern = /[A-Za-z0-9_][A-Za-z0-9_-]/
-    if (minimum and !minimum.match valid_pattern) || !maximum.match valid_pattern
-      logger.warn "find_commit_range called with string containing invalid characters: '#{minimum}', '#{maximum}'"
+    if (minimum and !git_check_ref_format(minimum)) or !git_check_ref_format(maximum)
+      logger.warn "find_commit_range called with invalid minimum or maximum: '#{minimum}', '#{maximum}'"
       return nil
     end
 
     if minimum and minimum.empty?
         minimum = nil
     end
-    
+
     if !maximum
       maximum = "HEAD"
     end
@@ -28,58 +36,52 @@ class Commit < ActiveRecord::Base
       readable = readable.where(name: repository)
     end
 
-    #puts "min #{minimum}"
-    #puts "max #{maximum}"
-    #puts "rep #{repository}"
-
     commits = []
     readable.each do |r|
       if on_disk_repos[r.name]
         ENV['GIT_DIR'] = on_disk_repos[r.name][:git_dir]
 
-        #puts "dir #{on_disk_repos[r.name][:git_dir]}"
-
         # We've filtered for invalid characters, so we can pass the contents of
         # minimum and maximum safely on the command line
 
-        #puts "git rev-list --max-count=1 #{maximum}"
-
         # Get the commit hash for the upper bound
         max_hash = nil
-        IO.foreach("|git rev-list --max-count=1 #{maximum}") do |line|
+        IO.foreach("|git rev-list --max-count=1 #{maximum.shellescape}") do |line|
           max_hash = line.strip
         end
 
         # If not found or string is invalid, nothing else to do
-        next if !max_hash or !max_hash.match valid_pattern
+        next if !max_hash or !git_check_ref_format(max_hash)
 
         resolved_exclude = nil
         if exclude
           resolved_exclude = []
           exclude.each do |e|
-            if e.match valid_pattern
-              IO.foreach("|git rev-list --max-count=1 #{e}") do |line|
+            if git_check_ref_format(e)
+              IO.foreach("|git rev-list --max-count=1 #{e.shellescape}") do |line|
                 resolved_exclude.push(line.strip)
               end
+            else
+              logger.warn "find_commit_range called with invalid exclude invalid characters: '#{exclude}'"
+              return nil
             end
           end
         end
 
-        if minimum          
+        if minimum
           # Get the commit hash for the lower bound
           min_hash = nil
-          IO.foreach("|git rev-list --max-count=1 #{minimum}") do |line|
+          IO.foreach("|git rev-list --max-count=1 #{minimum.shellescape}") do |line|
             min_hash = line.strip
           end
 
           # If not found or string is invalid, nothing else to do
-          next if !min_hash or !min_hash.match valid_pattern
-          
+          next if !min_hash or !git_check_ref_format(min_hash)
+
           # Now find all commits between them
-          #puts "git rev-list #{min_hash}..#{max_hash}"
-          IO.foreach("|git rev-list #{min_hash}..#{max_hash}") do |line|
+          IO.foreach("|git rev-list #{min_hash.shellescape}..#{max_hash.shellescape}") do |line|
             hash = line.strip
-            commits.push(hash) if !resolved_exclude or !resolved_exclude.include? hash              
+            commits.push(hash) if !resolved_exclude or !resolved_exclude.include? hash
           end
 
           commits.push(min_hash) if !resolved_exclude or !resolved_exclude.include? min_hash