Skip to content

Commit c26ad2a

Browse files
committed
Harden PPA defined type
Prior to this commit there was a possibility that malformed strings could be passed as the resources name. This could lead to unsafe executions on a remote system. This was also a possibility for the options parameter as it was constrained to a string. In addition, commands were not properly broken out in to arrays of arguments when passed to the exec resource. This commit fixes the above by adding validation to the resource name ensuring that the given ppa name conforms to expectation. Also, commands are now broken down in to arrays of arguments appropriately. This ensures safer execution on the remote system. Given that the options parameter, passed as a raw string, could lead to unsafe code execution it was reasonable to change the accepted type to an `Optional[Array[String]]. This means that an array of options can now be passed to the exec resource inside the original command.
1 parent 4b12e7b commit c26ad2a

File tree

6 files changed

+102
-58
lines changed

6 files changed

+102
-58
lines changed

examples/ppa.pp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
class { 'apt': }
22

33
# Example declaration of an Apt PPA
4-
apt::ppa { 'ppa:openstack-ppa/bleeding-edge': }
4+
apt::ppa { 'ppa:ubuntuhandbook1/apps': }

lib/facter/apt_sources.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
# This fact lists the .list filenames that are used by apt.
4+
Facter.add(:apt_sources) do
5+
confine osfamily: 'Debian'
6+
setcode do
7+
sources = ['sources.list']
8+
Dir.glob('/etc/apt/sources.list.d/*.list').each do |file|
9+
sources.push(File.basename(file))
10+
end
11+
sources
12+
end
13+
end

manifests/init.pp

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,14 @@
3737
# Configures various update settings. Valid options: a hash made up from the following keys:
3838
#
3939
# @option update [String] :frequency
40-
# Specifies how often to run `apt-get update`. If the exec resource `apt_update` is notified, `apt-get update` runs regardless of this value.
41-
# Valid options: 'always' (at every Puppet run); 'daily' (if the value of `apt_update_last_success` is less than current epoch time minus 86400);
42-
# 'weekly' (if the value of `apt_update_last_success` is less than current epoch time minus 604800); and 'reluctantly' (only if the exec resource
43-
# `apt_update` is notified). Default: 'reluctantly'.
40+
# Specifies how often to run `apt-get update`. If the exec resource `apt_update` is notified,
41+
# `apt-get update` runs regardless of this value.
42+
# Valid options:
43+
# 'always' (at every Puppet run);
44+
# daily' (if the value of `apt_update_last_success` is less than current epoch time minus 86400);
45+
# 'weekly' (if the value of `apt_update_last_success` is less than current epoch time minus 604800);
46+
# 'reluctantly' (only if the exec resource `apt_update` is notified).
47+
# Default: 'reluctantly'.
4448
#
4549
# @option update [Integer] :loglevel
4650
# Specifies the log level of logs outputted to the console. Default: undef.
@@ -122,38 +126,37 @@
122126
# Specifies whether to perform force purge or delete. Default false.
123127
#
124128
class apt (
125-
Hash $update_defaults = $apt::params::update_defaults,
126-
Hash $purge_defaults = $apt::params::purge_defaults,
127-
Hash $proxy_defaults = $apt::params::proxy_defaults,
128-
Hash $include_defaults = $apt::params::include_defaults,
129-
String $provider = $apt::params::provider,
130-
String $keyserver = $apt::params::keyserver,
131-
Optional[String] $key_options = $apt::params::key_options,
132-
Optional[String] $ppa_options = $apt::params::ppa_options,
133-
Optional[String] $ppa_package = $apt::params::ppa_package,
134-
Optional[Hash] $backports = $apt::params::backports,
135-
Hash $confs = $apt::params::confs,
136-
Hash $update = $apt::params::update,
137-
Hash $purge = $apt::params::purge,
138-
Apt::Proxy $proxy = $apt::params::proxy,
139-
Hash $sources = $apt::params::sources,
140-
Hash $keys = $apt::params::keys,
141-
Hash $ppas = $apt::params::ppas,
142-
Hash $pins = $apt::params::pins,
143-
Hash $settings = $apt::params::settings,
144-
Boolean $manage_auth_conf = $apt::params::manage_auth_conf,
145-
Array[Apt::Auth_conf_entry]
146-
$auth_conf_entries = $apt::params::auth_conf_entries,
147-
String $auth_conf_owner = $apt::params::auth_conf_owner,
148-
String $root = $apt::params::root,
149-
String $sources_list = $apt::params::sources_list,
150-
String $sources_list_d = $apt::params::sources_list_d,
151-
String $conf_d = $apt::params::conf_d,
152-
String $preferences = $apt::params::preferences,
153-
String $preferences_d = $apt::params::preferences_d,
154-
String $apt_conf_d = $apt::params::apt_conf_d,
155-
Hash $config_files = $apt::params::config_files,
156-
Boolean $sources_list_force = $apt::params::sources_list_force,
129+
Hash $update_defaults = $apt::params::update_defaults,
130+
Hash $purge_defaults = $apt::params::purge_defaults,
131+
Hash $proxy_defaults = $apt::params::proxy_defaults,
132+
Hash $include_defaults = $apt::params::include_defaults,
133+
String $provider = $apt::params::provider,
134+
String $keyserver = $apt::params::keyserver,
135+
Optional[String] $key_options = $apt::params::key_options,
136+
Optional[Array[String]] $ppa_options = $apt::params::ppa_options,
137+
Optional[String] $ppa_package = $apt::params::ppa_package,
138+
Optional[Hash] $backports = $apt::params::backports,
139+
Hash $confs = $apt::params::confs,
140+
Hash $update = $apt::params::update,
141+
Hash $purge = $apt::params::purge,
142+
Apt::Proxy $proxy = $apt::params::proxy,
143+
Hash $sources = $apt::params::sources,
144+
Hash $keys = $apt::params::keys,
145+
Hash $ppas = $apt::params::ppas,
146+
Hash $pins = $apt::params::pins,
147+
Hash $settings = $apt::params::settings,
148+
Boolean $manage_auth_conf = $apt::params::manage_auth_conf,
149+
Array[Apt::Auth_conf_entry] $auth_conf_entries = $apt::params::auth_conf_entries,
150+
String $auth_conf_owner = $apt::params::auth_conf_owner,
151+
String $root = $apt::params::root,
152+
String $sources_list = $apt::params::sources_list,
153+
String $sources_list_d = $apt::params::sources_list_d,
154+
String $conf_d = $apt::params::conf_d,
155+
String $preferences = $apt::params::preferences,
156+
String $preferences_d = $apt::params::preferences_d,
157+
String $apt_conf_d = $apt::params::apt_conf_d,
158+
Hash $config_files = $apt::params::config_files,
159+
Boolean $sources_list_force = $apt::params::sources_list_force,
157160

158161
Hash $source_key_defaults = {
159162
'server' => $keyserver,

manifests/params.pp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
'key' => '630239CC130E1A7FD81A27B140976EAF437D05B5',
9292
'repos' => 'main universe multiverse restricted',
9393
}
94-
$ppa_options = '-y'
94+
$ppa_options = ['-y']
9595
$ppa_package = 'software-properties-common'
9696
$auth_conf_owner = '_apt'
9797
}

manifests/ppa.pp

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424
# Specifies whether Puppet should manage the package that provides `apt-add-repository`.
2525
#
2626
define apt::ppa (
27-
String $ensure = 'present',
28-
Optional[String] $options = $::apt::ppa_options,
29-
Optional[String] $release = fact('os.distro.codename'),
30-
Optional[String] $dist = $facts['os']['name'],
31-
Optional[String] $package_name = $::apt::ppa_package,
32-
Boolean $package_manage = false,
27+
String $ensure = 'present',
28+
Optional[Array[String]] $options = $::apt::ppa_options,
29+
Optional[String] $release = fact('os.distro.codename'),
30+
Optional[String] $dist = $facts['os']['name'],
31+
Optional[String] $package_name = $::apt::ppa_package,
32+
Boolean $package_manage = false,
3333
) {
3434
unless $release {
3535
fail('os.distro.codename fact not available: release parameter required')
@@ -39,6 +39,11 @@
3939
fail('apt::ppa is not currently supported on Debian.')
4040
}
4141

42+
# Validate the resource name
43+
if $name !~ /^ppa:([a-zA-Z0-9\-_]+)\/([a-zA-z0-9\-_]+)$/ {
44+
fail("Invalid PPA name: ${name}")
45+
}
46+
4247
if versioncmp($facts['os']['release']['full'], '14.10') >= 0 {
4348
$distid = downcase($dist)
4449
$dash_filename = regsubst($name, '^ppa:([^/]+)/(.+)$', "\\1-${distid}-\\2")
@@ -62,6 +67,9 @@
6267
$trusted_gpg_d_filename = "${dash_filename_no_specialchars}.gpg"
6368
}
6469

70+
# This is the location of our main exec script
71+
$script_path = "/opt/puppetlabs/puppet/cache/add-apt-repository-${dash_filename_no_specialchars}-${release}.sh"
72+
6573
if $ensure == 'present' {
6674
if $package_manage {
6775
ensure_packages($package_name)
@@ -81,24 +89,36 @@
8189
$_proxy_env = []
8290
}
8391

84-
exec { "add-apt-repository-${name}":
85-
environment => $_proxy_env,
86-
command => "/usr/bin/add-apt-repository ${options} ${name} || (rm ${::apt::sources_list_d}/${sources_list_d_filename} && false)",
87-
unless => "/usr/bin/test -f ${::apt::sources_list_d}/${sources_list_d_filename} && /usr/bin/test -f ${::apt::trusted_gpg_d}/${trusted_gpg_d_filename}",
88-
user => 'root',
89-
logoutput => 'on_failure',
90-
notify => Class['apt::update'],
91-
require => $_require,
92-
}
92+
unless $sources_list_d_filename in $facts['apt_sources'] {
93+
$script_content = epp('apt/add-apt-repository.sh.epp', {
94+
command => ['/usr/bin/add-apt-repository', shell_join($options), $name],
95+
sources_list_d_path => $::apt::sources_list_d,
96+
sources_list_d_filename => $sources_list_d_filename,
97+
})
9398

94-
file { "${::apt::sources_list_d}/${sources_list_d_filename}":
95-
ensure => file,
96-
require => Exec["add-apt-repository-${name}"],
99+
file { "add-apt-repository-script-${name}":
100+
ensure => 'file',
101+
path => $script_path,
102+
content => $script_content,
103+
mode => '0755',
104+
}
105+
106+
exec { "add-apt-repository-${name}":
107+
environment => $_proxy_env,
108+
command => $script_path,
109+
logoutput => 'on_failure',
110+
notify => Class['apt::update'],
111+
require => $_require,
112+
}
97113
}
98114
}
99115
else {
100-
file { "${::apt::sources_list_d}/${sources_list_d_filename}":
101-
ensure => 'absent',
116+
tidy { "remove-apt-repository-script-${name}":
117+
path => $script_path,
118+
}
119+
120+
tidy { "remove-apt-repository-${name}":
121+
path => "${::apt::sources_list_d}/${sources_list_d_filename}",
102122
notify => Class['apt::update'],
103123
}
104124
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<%- | Array $command, String $sources_list_d_path, String $sources_list_d_filename | -%>
2+
3+
<%= $command.join(' ') %>
4+
5+
if [ $? -gt 0 ]; then
6+
rm <%= $sources_list_d_path %>/<%= $sources_list_d_filename %>
7+
exit 1
8+
fi

0 commit comments

Comments
 (0)