CVE-2017-10689
Description
In previous versions of Puppet Agent it was possible to install a module with world writable permissions. Puppet Agent 5.3.4 and 1.10.10 included a fix to this vulnerability.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Puppet Agent allowed world-writable permissions on extracted module files, potentially allowing local privilege escalation or unauthorized modification.
Vulnerability
In Puppet Agent versions prior to 5.3.4 and 1.10.10, the module unpacking code did not reset file permissions when extracting module tarballs [2][3]. This allowed files to retain their original permissions from the archive, which could include world-writable permissions for directories and non-executable files. The vulnerability is identified as CVE-2017-10689 and is documented in NVD [4]. The issue resides in the unpack method of lib/puppet/module_tool/tar.rb.
Exploitation
An attacker with the ability to install a Puppet module (e.g., via puppet module install) could craft a malicious module tarball containing files with world-writable permissions. No special network position is required if the attacker has local access or can trick a user into installing the module. The unpacking process would preserve the world-writable permissions from the archive.
Impact
A local attacker who gains write access to the installed module files could modify them, potentially leading to privilege escalation or arbitrary code execution in the context of the Puppet process or the system's Puppet user. This could compromise the integrity of the system's configuration management.
Mitigation
The fix was included in Puppet Agent versions 5.3.4 and 1.10.10 [2][3]. Red Hat issued an advisory (RHSA-2018:2927) for Red Hat Satellite 6.4 [1]. Users should upgrade to the latest patched versions. If upgrade is not possible, limit module installation to trusted sources.
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
puppetRubyGems | < 4.10.10 | 4.10.10 |
puppetRubyGems | >= 5.0.0, < 5.3.4 | 5.3.4 |
Affected products
7- ghsa-coords5 versionspkg:gem/puppetpkg:rpm/suse/puppet&distro=SUSE%20Linux%20Enterprise%20Desktop%2012%20SP2pkg:rpm/suse/puppet&distro=SUSE%20Linux%20Enterprise%20Desktop%2012%20SP3pkg:rpm/suse/puppet&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Advanced%20Systems%20Management%2012pkg:rpm/suse/rubygem-puppet&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Advanced%20Systems%20Management%2012
< 4.10.10+ 4 more
- (no CPE)range: < 4.10.10
- (no CPE)range: < 3.8.5-15.9.1
- (no CPE)range: < 3.8.5-15.9.1
- (no CPE)range: < 3.8.5-15.9.1
- (no CPE)range: < 4.8.1-32.3.1
- Puppet/Puppet Agentv5Range: prior to 5.3.4 or 1.10.10
- Puppet/Puppet Enterprisev5Range: prior to 2016.4.10 or 2017.3.4
Patches
22f1047f85e22Merge pull request #2 from puppetlabs/CVE-2017-2295-4.10.x
2 files changed · +91 −9
lib/puppet/module_tool/tar/mini.rb+57 −4 modified@@ -3,24 +3,77 @@ def unpack(sourcefile, destdir, _) Zlib::GzipReader.open(sourcefile) do |reader| Archive::Tar::Minitar.unpack(reader, destdir, find_valid_files(reader)) do |action, name, stats| case action - when :file_done - File.chmod(0644, "#{destdir}/#{name}") - when :dir, :file_start + when :dir validate_entry(destdir, name) + set_dir_mode!(stats) + Puppet.debug("Extracting: #{destdir}/#{name}") + when :file_start + # Octal string of the old file mode. + validate_entry(destdir, name) + set_file_mode!(stats) Puppet.debug("Extracting: #{destdir}/#{name}") end + set_default_user_and_group!(stats) + stats end end end def pack(sourcedir, destfile) Zlib::GzipWriter.open(destfile) do |writer| - Archive::Tar::Minitar.pack(sourcedir, writer) + Archive::Tar::Minitar.pack(sourcedir, writer) do |step, name, stats| + # TODO smcclellan 2017-10-31 Set permissions here when this yield block + # executes before the header is written. As it stands, the `stats` + # argument isn't mutable in a way that will effect the desired mode for + # the file. + end end end private + EXECUTABLE = 0755 + NOT_EXECUTABLE = 0644 + USER_EXECUTE = 0100 + + def set_dir_mode!(stats) + if stats.key?(:mode) + # This is only the case for `pack`, so this code will not run. + stats[:mode] = EXECUTABLE + elsif stats.key?(:entry) + old_mode = stats[:entry].instance_variable_get(:@mode) + if old_mode.is_a?(Fixnum) + stats[:entry].instance_variable_set(:@mode, EXECUTABLE) + end + end + end + + # Sets a file mode to 0755 if the file is executable by the user. + # Sets a file mode to 0644 if the file mode is set (non-Windows). + def sanitized_mode(old_mode) + old_mode & USER_EXECUTE != 0 ? EXECUTABLE : NOT_EXECUTABLE + end + + def set_file_mode!(stats) + if stats.key?(:mode) + # This is only the case for `pack`, so this code will not run. + stats[:mode] = sanitized_mode(stats[:mode]) + elsif stats.key?(:entry) + old_mode = stats[:entry].instance_variable_get(:@mode) + # If the user can execute the file, set 0755, otherwise 0644. + if old_mode.is_a?(Fixnum) + new_mode = sanitized_mode(old_mode) + stats[:entry].instance_variable_set(:@mode, new_mode) + end + end + end + + # Sets UID and GID to 0 for standardization. + def set_default_user_and_group!(stats) + stats[:uid] = 0 + stats[:gid] = 0 + end + # Find all the valid files in tarfile. # # This check was mainly added to ignore 'x' and 'g' flags from the PAX
spec/unit/module_tool/tar/mini_spec.rb+34 −5 modified@@ -8,10 +8,17 @@ let(:destfile) { '/the/dest/file.tar.gz' } let(:minitar) { described_class.new } - it "unpacks a tar file" do - unpacks_the_entry(:file_start, 'thefile') + class MockFileStatEntry + def initialize(mode = 0100) + @mode = mode + end + end + + it "unpacks a tar file with correct permissions" do + entry = unpacks_the_entry(:file_start, 'thefile') minitar.unpack(sourcefile, destdir, 'uid') + expect(entry.instance_variable_get(:@mode)).to eq(0755) end it "does not allow an absolute path" do @@ -41,20 +48,42 @@ "Attempt to install file with an invalid path into \"#{File.expand_path('/the/thedir')}\" under \"#{destdir}\"") end + it "unpacks on Windows" do + unpacks_the_entry(:file_start, 'thefile', nil) + + entry = minitar.unpack(sourcefile, destdir, 'uid') + # Windows does not use these permissions. + expect(entry.instance_variable_get(:@mode)).to eq(nil) + end + it "packs a tar file" do writer = stub('GzipWriter') Zlib::GzipWriter.expects(:open).with(destfile).yields(writer) - Archive::Tar::Minitar.expects(:pack).with(sourcedir, writer) + stats = {:mode => 0222} + Archive::Tar::Minitar.expects(:pack).with(sourcedir, writer).yields(:file_start, 'abc', stats) + + minitar.pack(sourcedir, destfile) + end + + it "packs a tar file on Windows" do + writer = stub('GzipWriter') + + Zlib::GzipWriter.expects(:open).with(destfile).yields(writer) + Archive::Tar::Minitar.expects(:pack).with(sourcedir, writer). + yields(:file_start, 'abc', {:entry => MockFileStatEntry.new(nil)}) minitar.pack(sourcedir, destfile) end - def unpacks_the_entry(type, name) + def unpacks_the_entry(type, name, mode = 0100) reader = stub('GzipReader') Zlib::GzipReader.expects(:open).with(sourcefile).yields(reader) minitar.expects(:find_valid_files).with(reader).returns([name]) - Archive::Tar::Minitar.expects(:unpack).with(reader, destdir, [name]).yields(type, name, nil) + entry = MockFileStatEntry.new(mode) + Archive::Tar::Minitar.expects(:unpack).with(reader, destdir, [name]). + yields(type, name, {:entry => entry}) + entry end end
17d9e02da388(PUP-7866) Reset permissions when unpacking tar in PMT
2 files changed · +91 −9
lib/puppet/module_tool/tar/mini.rb+57 −4 modified@@ -3,24 +3,77 @@ def unpack(sourcefile, destdir, _) Zlib::GzipReader.open(sourcefile) do |reader| Archive::Tar::Minitar.unpack(reader, destdir, find_valid_files(reader)) do |action, name, stats| case action - when :file_done - File.chmod(0644, "#{destdir}/#{name}") - when :dir, :file_start + when :dir validate_entry(destdir, name) + set_dir_mode!(stats) + Puppet.debug("Extracting: #{destdir}/#{name}") + when :file_start + # Octal string of the old file mode. + validate_entry(destdir, name) + set_file_mode!(stats) Puppet.debug("Extracting: #{destdir}/#{name}") end + set_default_user_and_group!(stats) + stats end end end def pack(sourcedir, destfile) Zlib::GzipWriter.open(destfile) do |writer| - Archive::Tar::Minitar.pack(sourcedir, writer) + Archive::Tar::Minitar.pack(sourcedir, writer) do |step, name, stats| + # TODO smcclellan 2017-10-31 Set permissions here when this yield block + # executes before the header is written. As it stands, the `stats` + # argument isn't mutable in a way that will effect the desired mode for + # the file. + end end end private + EXECUTABLE = 0755 + NOT_EXECUTABLE = 0644 + USER_EXECUTE = 0100 + + def set_dir_mode!(stats) + if stats.key?(:mode) + # This is only the case for `pack`, so this code will not run. + stats[:mode] = EXECUTABLE + elsif stats.key?(:entry) + old_mode = stats[:entry].instance_variable_get(:@mode) + if old_mode.is_a?(Fixnum) + stats[:entry].instance_variable_set(:@mode, EXECUTABLE) + end + end + end + + # Sets a file mode to 0755 if the file is executable by the user. + # Sets a file mode to 0644 if the file mode is set (non-Windows). + def sanitized_mode(old_mode) + old_mode & USER_EXECUTE != 0 ? EXECUTABLE : NOT_EXECUTABLE + end + + def set_file_mode!(stats) + if stats.key?(:mode) + # This is only the case for `pack`, so this code will not run. + stats[:mode] = sanitized_mode(stats[:mode]) + elsif stats.key?(:entry) + old_mode = stats[:entry].instance_variable_get(:@mode) + # If the user can execute the file, set 0755, otherwise 0644. + if old_mode.is_a?(Fixnum) + new_mode = sanitized_mode(old_mode) + stats[:entry].instance_variable_set(:@mode, new_mode) + end + end + end + + # Sets UID and GID to 0 for standardization. + def set_default_user_and_group!(stats) + stats[:uid] = 0 + stats[:gid] = 0 + end + # Find all the valid files in tarfile. # # This check was mainly added to ignore 'x' and 'g' flags from the PAX
spec/unit/module_tool/tar/mini_spec.rb+34 −5 modified@@ -8,10 +8,17 @@ let(:destfile) { '/the/dest/file.tar.gz' } let(:minitar) { described_class.new } - it "unpacks a tar file" do - unpacks_the_entry(:file_start, 'thefile') + class MockFileStatEntry + def initialize(mode = 0100) + @mode = mode + end + end + + it "unpacks a tar file with correct permissions" do + entry = unpacks_the_entry(:file_start, 'thefile') minitar.unpack(sourcefile, destdir, 'uid') + expect(entry.instance_variable_get(:@mode)).to eq(0755) end it "does not allow an absolute path" do @@ -41,20 +48,42 @@ "Attempt to install file with an invalid path into \"#{File.expand_path('/the/thedir')}\" under \"#{destdir}\"") end + it "unpacks on Windows" do + unpacks_the_entry(:file_start, 'thefile', nil) + + entry = minitar.unpack(sourcefile, destdir, 'uid') + # Windows does not use these permissions. + expect(entry.instance_variable_get(:@mode)).to eq(nil) + end + it "packs a tar file" do writer = stub('GzipWriter') Zlib::GzipWriter.expects(:open).with(destfile).yields(writer) - Archive::Tar::Minitar.expects(:pack).with(sourcedir, writer) + stats = {:mode => 0222} + Archive::Tar::Minitar.expects(:pack).with(sourcedir, writer).yields(:file_start, 'abc', stats) + + minitar.pack(sourcedir, destfile) + end + + it "packs a tar file on Windows" do + writer = stub('GzipWriter') + + Zlib::GzipWriter.expects(:open).with(destfile).yields(writer) + Archive::Tar::Minitar.expects(:pack).with(sourcedir, writer). + yields(:file_start, 'abc', {:entry => MockFileStatEntry.new(nil)}) minitar.pack(sourcedir, destfile) end - def unpacks_the_entry(type, name) + def unpacks_the_entry(type, name, mode = 0100) reader = stub('GzipReader') Zlib::GzipReader.expects(:open).with(sourcefile).yields(reader) minitar.expects(:find_valid_files).with(reader).returns([name]) - Archive::Tar::Minitar.expects(:unpack).with(reader, destdir, [name]).yields(type, name, nil) + entry = MockFileStatEntry.new(mode) + Archive::Tar::Minitar.expects(:unpack).with(reader, destdir, [name]). + yields(type, name, {:entry => entry}) + entry end end
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- access.redhat.com/errata/RHSA-2018:2927ghsavendor-advisoryx_refsource_REDHATWEB
- github.com/advisories/GHSA-vw22-465p-8j5wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2017-10689ghsaADVISORY
- usn.ubuntu.com/3567-1/mitrevendor-advisoryx_refsource_UBUNTU
- github.com/puppetlabs/puppet/commit/17d9e02da3882e44c1876e2805cf9708481715eeghsaWEB
- github.com/puppetlabs/puppet/commit/2f1047f85e22cde139a421bc25d371f2ffc92cb1ghsaWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/puppet/CVE-2017-10689.ymlghsaWEB
- puppet.com/security/cve/CVE-2017-10689ghsax_refsource_CONFIRMWEB
- tickets.puppetlabs.com/browse/PUP-7866ghsaWEB
- usn.ubuntu.com/3567-1ghsaWEB
News mentions
0No linked articles in our index yet.