Skip to content

Commit 1044a32

Browse files
authored
Merge pull request #1240 from DavidS/fix-command-escaping
Fix command shell escaping
2 parents 25b816e + 08a1752 commit 1044a32

File tree

10 files changed

+87
-109
lines changed

10 files changed

+87
-109
lines changed

lib/puppet/provider/postgresql_psql/ruby.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def run_sql_command(sql)
1616
command = [resource[:psql_path]]
1717
command.push('-d', resource[:db]) if resource[:db]
1818
command.push('-p', resource[:port]) if resource[:port]
19-
command.push('-t', '-X', '-c', '"' + sql.gsub('"', '\"') + '"')
19+
command.push('-t', '-X', '-c', sql)
2020

2121
environment = fetch_environment
2222

@@ -57,13 +57,13 @@ def fetch_environment
5757
end
5858

5959
def run_command(command, user, group, environment)
60-
command = command.join ' '
6160
output = Puppet::Util::Execution.execute(command, uid: user,
6261
gid: group,
6362
failonfail: false,
6463
combine: true,
6564
override_locale: true,
66-
custom_environment: environment)
65+
custom_environment: environment,
66+
sensitive: resource[:sensitive] == :true)
6767
[output, $CHILD_STATUS.dup]
6868
end
6969
end

lib/puppet/type/postgresql_psql.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ def matches(value)
124124
newvalues(:true, :false)
125125
end
126126

127+
newparam(:sensitive, boolean: true) do
128+
desc "If 'true', then the executed command will not be echoed into the log. Use this to protect sensitive information passing through."
129+
130+
defaultto(:false)
131+
newvalues(:true, :false)
132+
end
133+
127134
autorequire(:class) { ['Postgresql::Server::Service'] }
128135

129136
def should_run_sql(refreshing = false)

lib/puppet/util/postgresql_validator.rb

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,41 @@ def initialize(resource)
1010
end
1111

1212
def build_psql_cmd
13-
final_cmd = []
14-
15-
cmd_init = "#{@resource[:psql_path]} --tuples-only --quiet --no-psqlrc"
16-
17-
final_cmd.push cmd_init
18-
19-
cmd_parts = {
20-
host: "--host #{@resource[:host]}",
21-
port: "--port #{@resource[:port]}",
22-
db_username: "--username #{@resource[:db_username]}",
23-
db_name: "--dbname #{@resource[:db_name]}",
24-
command: "--command '#{@resource[:command]}'",
13+
cmd = [@resource[:psql_path], '--tuples-only', '--quiet', '--no-psqlrc']
14+
15+
args = {
16+
host: '--host',
17+
port: '--port',
18+
db_username: '--username',
19+
db_name: '--dbname',
20+
command: '--command',
2521
}
2622

27-
cmd_parts.each do |k, v|
28-
final_cmd.push v if @resource[k]
23+
args.each do |k, v|
24+
if @resource[k]
25+
cmd.push v
26+
cmd.push @resource[k]
27+
end
2928
end
3029

31-
final_cmd.join ' '
30+
cmd
3231
end
3332

34-
def parse_connect_settings
35-
c_settings = @resource[:connect_settings] || {}
36-
c_settings['PGPASSWORD'] = @resource[:db_password] if @resource[:db_password]
37-
c_settings.map { |k, v| "#{k}=#{v}" }
33+
def connect_settings
34+
result = @resource[:connect_settings] || {}
35+
result['PGPASSWORD'] = @resource[:db_password] if @resource[:db_password]
36+
result
3837
end
3938

4039
def attempt_connection(sleep_length, tries)
4140
(0..tries - 1).each do |_try|
4241
Puppet.debug "PostgresqlValidator.attempt_connection: Attempting connection to #{@resource[:db_name]}"
43-
Puppet.debug "PostgresqlValidator.attempt_connection: #{build_validate_cmd}"
44-
result = execute_command
42+
cmd = build_psql_cmd
43+
Puppet.debug "PostgresqlValidator.attempt_connection: #{cmd.inspect}"
44+
result = Execution.execute(cmd, custom_environment: connect_settings, uid: @resource[:run_as])
45+
4546
if result && !result.empty?
46-
Puppet.debug "PostgresqlValidator.attempt_connection: Connection to #{@resource[:db_name] || parse_connect_settings.select { |elem| elem.include?('PGDATABASE') }} successful!"
47+
Puppet.debug "PostgresqlValidator.attempt_connection: Connection to #{@resource[:db_name] || connect_settings.select { |elem| elem.include?('PGDATABASE') }} successful!"
4748
return true
4849
else
4950
Puppet.warning "PostgresqlValidator.attempt_connection: Sleeping for #{sleep_length} seconds"
@@ -52,15 +53,5 @@ def attempt_connection(sleep_length, tries)
5253
end
5354
false
5455
end
55-
56-
private
57-
58-
def execute_command
59-
Execution.execute(build_validate_cmd, uid: @resource[:run_as])
60-
end
61-
62-
def build_validate_cmd
63-
"#{parse_connect_settings.join(' ')} #{build_psql_cmd} "
64-
end
6556
end
6657
end

manifests/server/role.pp

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
# @param update_password If set to true, updates the password on changes. Set this to false to not modify the role's password after creation.
44
# @param password_hash Sets the hash to use during password creation.
55
# @param createdb Specifies whether to grant the ability to create new databases with this role.
6-
# @param createrole Specifies whether to grant the ability to create new roles with this role.
7-
# @param db Database used to connect to.
6+
# @param createrole Specifies whether to grant the ability to create new roles with this role.
7+
# @param db Database used to connect to.
88
# @param port Port to use when connecting.
99
# @param login Specifies whether to grant login capability for the new role.
1010
# @param inherit Specifies whether to grant inherit capability for the new role.
@@ -76,18 +76,16 @@
7676
$superuser_sql = $superuser ? { true => 'SUPERUSER', default => 'NOSUPERUSER' }
7777
$replication_sql = $replication ? { true => 'REPLICATION', default => '' }
7878
if ($password_hash != false) {
79-
$environment = "NEWPGPASSWD=${password_hash}"
80-
$password_sql = "ENCRYPTED PASSWORD '\$NEWPGPASSWD'"
79+
$password_sql = "ENCRYPTED PASSWORD '${password_hash}'"
8180
} else {
8281
$password_sql = ''
83-
$environment = []
8482
}
8583

8684
postgresql_psql { "CREATE ROLE ${username} ENCRYPTED PASSWORD ****":
87-
command => "CREATE ROLE \"${username}\" ${password_sql} ${login_sql} ${createrole_sql} ${createdb_sql} ${superuser_sql} ${replication_sql} CONNECTION LIMIT ${connection_limit}",
85+
command => Sensitive("CREATE ROLE \"${username}\" ${password_sql} ${login_sql} ${createrole_sql} ${createdb_sql} ${superuser_sql} ${replication_sql} CONNECTION LIMIT ${connection_limit}"),
8886
unless => "SELECT 1 FROM pg_roles WHERE rolname = '${username}'",
89-
environment => $environment,
9087
require => undef,
88+
sensitive => true,
9189
}
9290

9391
postgresql_psql { "ALTER ROLE \"${username}\" ${superuser_sql}":
@@ -134,9 +132,9 @@
134132
$pwd_hash_sql = "md5${pwd_md5}"
135133
}
136134
postgresql_psql { "ALTER ROLE ${username} ENCRYPTED PASSWORD ****":
137-
command => "ALTER ROLE \"${username}\" ${password_sql}",
138-
unless => "SELECT 1 FROM pg_shadow WHERE usename = '${username}' AND passwd = '${pwd_hash_sql}'",
139-
environment => $environment,
135+
command => Sensitive("ALTER ROLE \"${username}\" ${password_sql}"),
136+
unless => Sensitive("SELECT 1 FROM pg_shadow WHERE usename = '${username}' AND passwd = '${pwd_hash_sql}'"),
137+
sensitive => true,
140138
}
141139
}
142140
} else {

spec/acceptance/postgresql_psql_spec.rb

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -134,39 +134,25 @@ class { 'postgresql::server': } ->
134134
apply_manifest(pp_nine, expect_changes: true)
135135
end
136136

137-
context 'with secure password passing by environment' do
138-
it 'runs SQL that contanins password passed by environment' do
139-
select = "select \\'$PASS_TO_EMBED\\'"
140-
pp = <<-MANIFEST.unindent
137+
context 'when setting sensitive => true' do
138+
it 'runs queries without leaking to the log' do
139+
select = "select \\'pa$swD\\'"
140+
pp = <<~MANIFEST
141141
class { 'postgresql::server': } ->
142-
postgresql_psql { 'password embedded by environment: #{select}':
142+
postgresql_psql { 'password protected by sensitive: #{select}':
143143
db => 'postgres',
144144
psql_user => 'postgres',
145+
sensitive => true,
145146
command => '#{select}',
146-
environment => [
147-
'PASS_TO_EMBED=pa$swD',
148-
],
149-
}
150-
MANIFEST
151-
apply_manifest(pp, catch_failures: true)
152-
apply_manifest(pp, expect_changes: false)
153-
end
154-
it 'runs SQL that contanins password passed by environment in check' do
155-
select = "select 1 where \\'$PASS_TO_EMBED\\'=\\'passwD\\'"
156-
pp = <<-MANIFEST.unindent
157-
class { 'postgresql::server': } ->
158-
postgresql_psql { 'password embedded by environment in check: #{select}':
159-
db => 'postgres',
160-
psql_user => 'postgres',
161-
command => 'invalid sql query',
162-
unless => '#{select}',
163-
environment => [
164-
'PASS_TO_EMBED=passwD',
165-
],
166147
}
167148
MANIFEST
149+
result = apply_manifest(pp, catch_failures: true, debug: true)
150+
expect(result.stdout).not_to contain('pa$swD')
151+
expect(result.stderr).not_to contain('pa$swD')
168152

169-
idempotent_apply(pp)
153+
result = apply_manifest(pp, expect_changes: false, debug: true)
154+
expect(result.stdout).not_to contain('pa$swD')
155+
expect(result.stderr).not_to contain('pa$swD')
170156
end
171157
end
172158
end

spec/spec_helper_local.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
# this could definitely be optimized
3434
add_filter do |f|
3535
# system returns true if exit status is 0, which with git-check-ignore means file is ignored
36-
system("git check-ignore --quiet #{f.filename}")
36+
system('git', 'check-ignore', '--quiet', f.filename)
3737
end
3838
end
3939
end

spec/unit/defines/server/role_spec.rb

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@
3333
it { is_expected.to contain_postgresql__server__role('test') }
3434
it 'has create role for "test" user with password as ****' do
3535
is_expected.to contain_postgresql_psql('CREATE ROLE test ENCRYPTED PASSWORD ****')
36-
.with('command' => "CREATE ROLE \"test\" ENCRYPTED PASSWORD '$NEWPGPASSWD' LOGIN NOCREATEROLE NOCREATEDB NOSUPERUSER CONNECTION LIMIT -1",
37-
'environment' => 'NEWPGPASSWD=new-pa$s',
36+
.with('command' => 'Sensitive [value redacted]',
37+
'sensitive' => 'true',
3838
'unless' => "SELECT 1 FROM pg_roles WHERE rolname = 'test'",
3939
'port' => '5432')
4040
end
4141
it 'has alter role for "test" user with password as ****' do
4242
is_expected.to contain_postgresql_psql('ALTER ROLE test ENCRYPTED PASSWORD ****')
43-
.with('command' => "ALTER ROLE \"test\" ENCRYPTED PASSWORD '$NEWPGPASSWD'",
44-
'environment' => 'NEWPGPASSWD=new-pa$s',
45-
'unless' => "SELECT 1 FROM pg_shadow WHERE usename = 'test' AND passwd = 'md5b6f7fcbbabb4befde4588a26c1cfd2fa'",
43+
.with('command' => 'Sensitive [value redacted]',
44+
'sensitive' => 'true',
45+
'unless' => 'Sensitive [value redacted]',
4646
'port' => '5432')
4747
end
4848

@@ -64,17 +64,17 @@
6464
it { is_expected.to contain_postgresql__server__role('test') }
6565
it 'has create role for "test" user with password as ****' do
6666
is_expected.to contain_postgresql_psql('CREATE ROLE test ENCRYPTED PASSWORD ****')
67-
.with_command("CREATE ROLE \"test\" ENCRYPTED PASSWORD '$NEWPGPASSWD' LOGIN NOCREATEROLE NOCREATEDB NOSUPERUSER CONNECTION LIMIT -1")
68-
.with_environment('NEWPGPASSWD=new-pa$s')
67+
.with_command('Sensitive [value redacted]')
68+
.with_sensitive('true')
6969
.with_unless("SELECT 1 FROM pg_roles WHERE rolname = 'test'")
7070
.with_port(5432)
7171
.with_connect_settings('PGHOST' => 'postgres-db-server', 'DBVERSION' => '9.1', 'PGUSER' => 'login-user', 'PGPASSWORD' => 'login-pass')
7272
.that_requires('Class[postgresql::server::service]')
7373
end
7474
it 'has alter role for "test" user with password as ****' do
7575
is_expected.to contain_postgresql_psql('ALTER ROLE test ENCRYPTED PASSWORD ****')
76-
.with('command' => "ALTER ROLE \"test\" ENCRYPTED PASSWORD '$NEWPGPASSWD'", 'environment' => 'NEWPGPASSWD=new-pa$s',
77-
'unless' => "SELECT 1 FROM pg_shadow WHERE usename = 'test' AND passwd = 'md5b6f7fcbbabb4befde4588a26c1cfd2fa'", 'port' => '5432',
76+
.with('command' => 'Sensitive [value redacted]', 'sensitive' => 'true',
77+
'unless' => 'Sensitive [value redacted]', 'port' => '5432',
7878
'connect_settings' => { 'PGHOST' => 'postgres-db-server', 'DBVERSION' => '9.1',
7979
'PGUSER' => 'login-user', 'PGPASSWORD' => 'login-pass' })
8080
end
@@ -99,15 +99,15 @@
9999
it { is_expected.to contain_postgresql__server__role('test') }
100100
it 'has create role for "test" user with password as ****' do
101101
is_expected.to contain_postgresql_psql('CREATE ROLE test ENCRYPTED PASSWORD ****')
102-
.with('command' => "CREATE ROLE \"test\" ENCRYPTED PASSWORD '$NEWPGPASSWD' LOGIN NOCREATEROLE NOCREATEDB NOSUPERUSER CONNECTION LIMIT -1",
103-
'environment' => 'NEWPGPASSWD=new-pa$s', 'unless' => "SELECT 1 FROM pg_roles WHERE rolname = 'test'",
104-
'connect_settings' => { 'PGHOST' => 'postgres-db-server', 'DBVERSION' => '9.1',
105-
'PGPORT' => '1234', 'PGUSER' => 'login-user', 'PGPASSWORD' => 'login-pass' })
102+
.with('command' => 'Sensitive [value redacted]',
103+
'sensitive' => 'true', 'unless' => "SELECT 1 FROM pg_roles WHERE rolname = 'test'",
104+
'connect_settings' => { 'PGHOST' => 'postgres-db-server', 'DBVERSION' => '9.1',
105+
'PGPORT' => '1234', 'PGUSER' => 'login-user', 'PGPASSWORD' => 'login-pass' })
106106
end
107107
it 'has alter role for "test" user with password as ****' do
108108
is_expected.to contain_postgresql_psql('ALTER ROLE test ENCRYPTED PASSWORD ****')
109-
.with('command' => "ALTER ROLE \"test\" ENCRYPTED PASSWORD '$NEWPGPASSWD'", 'environment' => 'NEWPGPASSWD=new-pa$s',
110-
'unless' => "SELECT 1 FROM pg_shadow WHERE usename = 'test' AND passwd = 'md5b6f7fcbbabb4befde4588a26c1cfd2fa'",
109+
.with('command' => 'Sensitive [value redacted]', 'sensitive' => 'true',
110+
'unless' => 'Sensitive [value redacted]',
111111
'connect_settings' => { 'PGHOST' => 'postgres-db-server', 'DBVERSION' => '9.1',
112112
'PGPORT' => '1234', 'PGUSER' => 'login-user', 'PGPASSWORD' => 'login-pass' })
113113
end

spec/unit/puppet/provider/postgresql_conn_validator/ruby_spec.rb

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,37 +30,33 @@
3030

3131
describe '#build_psql_cmd' do
3232
it 'contains expected commandline options' do
33-
expect(provider.validator.build_psql_cmd).to match %r{/usr/bin/psql.*--host.*--port.*--username.*}
33+
expect(provider.validator.build_psql_cmd).to eq(['/usr/bin/psql', '--tuples-only', '--quiet', '--no-psqlrc', '--host', 'db.test.com', '--port', 4444, '--username', 'testuser', '--command',
34+
'SELECT 1'])
3435
end
3536
end
3637

37-
describe '#parse_connect_settings' do
38+
describe 'connect_settings' do
3839
it 'returns array if password is present' do
39-
expect(provider.validator.parse_connect_settings).to eq(['PGPASSWORD=testpass'])
40+
expect(provider.validator.connect_settings).to eq({ 'PGPASSWORD' => 'testpass' })
4041
end
4142

4243
it 'returns an empty array if password is nil' do
4344
attributes.delete(:db_password)
44-
expect(provider.validator.parse_connect_settings).to eq([])
45+
expect(provider.validator.connect_settings).to eq({})
4546
end
4647

4748
it 'returns an array of settings' do
4849
attributes.delete(:db_password)
4950
attributes.merge! connect_settings
50-
expect(provider.validator.parse_connect_settings).to eq(['PGPASSWORD=testpass', 'PGHOST=db.test.com', 'PGPORT=1234'])
51+
expect(provider.validator.connect_settings).to eq({ PGHOST: 'db.test.com', PGPASSWORD: 'testpass', PGPORT: '1234' })
5152
end
5253
end
5354

5455
describe '#attempt_connection' do
5556
let(:sleep_length) { 1 }
5657
let(:tries) { 3 }
57-
let(:exec) do
58-
provider.validator.stub(:execute_command).and_return(true)
59-
end
6058

6159
it 'tries the correct number of times' do
62-
expect(provider.validator).to receive(:execute_command).exactly(3).times
63-
6460
provider.validator.attempt_connection(sleep_length, tries)
6561
end
6662
end

spec/unit/puppet/provider/postgresql_psql/ruby_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
it 'executes with the given psql_path on the given DB' do
1717
expect(provider).to receive(:run_command).with(['psql', '-d',
18-
attributes[:db], '-t', '-X', '-c', '"SELECT \'something\' as \"Custom column\""'], 'postgres',
18+
attributes[:db], '-t', '-X', '-c', 'SELECT \'something\' as "Custom column"'], 'postgres',
1919
'postgres', {})
2020

2121
provider.run_sql_command('SELECT \'something\' as "Custom column"')
@@ -35,7 +35,7 @@
3535
it 'executes with the given psql_path on the given DB' do
3636
expect(Dir).to receive(:chdir).with(attributes[:cwd]).and_yield
3737
expect(provider).to receive(:run_command).with([attributes[:psql_path],
38-
'-d', attributes[:db], '-t', '-X', '-c', '"SELECT \'something\' as \"Custom column\""'],
38+
'-d', attributes[:db], '-t', '-X', '-c', 'SELECT \'something\' as "Custom column"'],
3939
attributes[:psql_user], attributes[:psql_group], {})
4040

4141
provider.run_sql_command('SELECT \'something\' as "Custom column"')
@@ -50,7 +50,7 @@
5050

5151
it 'executes with the given search_path' do
5252
expect(provider).to receive(:run_command).with(['psql', '-t', '-X', '-c',
53-
'"set search_path to schema1; SELECT \'something\' as \"Custom column\""'],
53+
'set search_path to schema1; SELECT \'something\' as "Custom column"'],
5454
'postgres', 'postgres', {})
5555

5656
provider.run_sql_command('SELECT \'something\' as "Custom column"')
@@ -65,7 +65,7 @@
6565

6666
it 'executes with the given search_path' do
6767
expect(provider).to receive(:run_command).with(['psql', '-t', '-X', '-c',
68-
'"set search_path to schema1,schema2; SELECT \'something\' as \"Custom column\""'],
68+
'set search_path to schema1,schema2; SELECT \'something\' as "Custom column"'],
6969
'postgres', 'postgres',
7070
{})
7171

@@ -79,7 +79,7 @@
7979
it 'executes with the given port' do
8080
expect(provider).to receive(:run_command).with(['psql',
8181
'-p', '5555',
82-
'-t', '-X', '-c', '"SELECT something"'],
82+
'-t', '-X', '-c', 'SELECT something'],
8383
'postgres', 'postgres', {})
8484

8585
provider.run_sql_command('SELECT something')
@@ -91,7 +91,7 @@
9191
it 'executes with the given host' do
9292
expect(provider).to receive(:run_command).with(['psql',
9393
'-t', '-X', '-c',
94-
'"SELECT something"'],
94+
'SELECT something'],
9595
'postgres', 'postgres', 'PGHOST' => '127.0.0.1')
9696

9797
provider.run_sql_command('SELECT something')

0 commit comments

Comments
 (0)