CVE-2012-3865 (published 2012-08-06T16:55Z)

Directory traversal vulnerability in lib/puppet/reports/store.rb in Puppet before 2.6.17 and 2.7.x before 2.7.18, and Puppet Enterprise before 2.5.2, when Delete is enabled in auth.conf, allows remote authenticated users to delete arbitrary files on the puppet master server via a .. (dot dot) in a node name.

Severity: LOW
CVSS v2 base score: 3.5 (AV:N/AC:M/Au:S/C:N/I:N/A:P)

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/puppetlabs/puppet
commit_details: repo_url: https://github.com/puppetlabs/puppet
author: Patrick Carlisle
author_date: 2012-06-28 14:08:55-07:00
msg: Reject directory traversal in store report processor
repository: repo_url: https://github.com/puppetlabs/puppet
repo_name: puppetlabs/puppet
description: Server automation framework and application
repo_language: Ruby
owner: puppetlabs
file_changes: filename: store.rb
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,8 @@ require 'puppet'
 require 'fileutils'
 require 'tempfile'
 
+SEPARATOR = [Regexp.escape(File::SEPARATOR.to_s), Regexp.escape(File::ALT_SEPARATOR.to_s)].join
+
 Puppet::Reports.register_report(:store) do
   desc "Store the yaml report on disk.  Each host sends its report as a YAML dump
     and this just stores the file on disk, in the `reportdir` directory.
@@ -13,9 +15,11 @@ Puppet::Reports.register_report(:store) do
   def process
     # We don't want any tracking back in the fs.  Unlikely, but there
     # you go.
-    client = self.host.gsub("..",".")
+    if host =~ Regexp.union(/[#{SEPARATOR}]/, /\A\.\.?\Z/)
+      raise ArgumentError, "Invalid node name #{host.inspect}"
+    end
 
-    dir = File.join(Puppet[:reportdir], client)
+    dir = File.join(Puppet[:reportdir], host)
 
     if ! FileTest.exists?(dir)
       FileUtils.mkdir_p(dir)
@@ -42,17 +46,20 @@ Puppet::Reports.register_report(:store) do
       FileUtils.mv(f.path, file)
     rescue => detail
       puts detail.backtrace if Puppet[:trace]
-      Puppet.warning "Could not write report for #{client} at #{file}: #{detail}"
+      Puppet.warning "Could not write report for #{host} at #{file}: #{detail}"
     end
 
     # Only testing cares about the return value
     file
   end
 
-  # removes all reports for a given host
+  # removes all reports for a given host?
   def self.destroy(host)
-    client = host.gsub("..",".")
-    dir = File.join(Puppet[:reportdir], client)
+    if host =~ Regexp.union(/[#{SEPARATOR}]/, /\A\.\.?\Z/)
+      raise ArgumentError, "Invalid node name #{host.inspect}"
+    end
+
+    dir = File.join(Puppet[:reportdir], host)
 
     if File.exists?(dir)
       Dir.entries(dir).each do |file|

programming_language: Ruby
filename: store_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -44,5 +44,33 @@ describe processor do
       FileUtils.expects(:mv).in_sequence(writeseq).with(File.join(Dir.tmpdir, "foo123"), File.join(Puppet[:reportdir], @report.host, "201101061200.yaml"))
       @report.process
     end
+
+    ['..', 'hello/', '/hello', 'he/llo', 'hello/..', '.'].each do |node|
+      it "rejects #{node.inspect}" do
+        @report.host = node
+        expect { @report.process }.to raise_error(ArgumentError, /Invalid node/)
+      end
+    end
+
+    ['.hello', 'hello.', '..hi', 'hi..'].each do |node|
+      it "accepts #{node.inspect}" do
+        @report.host = node
+        @report.process
+      end
+    end
+  end
+
+  describe "::destroy" do
+    ['..', 'hello/', '/hello', 'he/llo', 'hello/..', '.'].each do |node|
+      it "rejects #{node.inspect}" do
+        expect { processor.destroy(node) }.to raise_error(ArgumentError, /Invalid node/)
+      end
+    end
+
+    ['.hello', 'hello.', '..hi', 'hi..'].each do |node|
+      it "accepts #{node.inspect}" do
+        processor.destroy(node)
+      end
+    end
   end
 end

programming_language: Ruby