Skip to content

Commit f843286

Browse files
authored
Merge pull request #48 from smortex/fix-safe-interpolation-detection
Fix safe interpolation detection
2 parents 59808fa + 80a991d commit f843286

File tree

3 files changed

+96
-9
lines changed

3 files changed

+96
-9
lines changed

lib/puppet-lint/plugins/check_unsafe_interpolations.rb

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@ def check
2222
end
2323
end
2424

25-
# Iterate over the tokens in a title and raise a warning if an interpolated variable is found
25+
# Iterate over the tokens in a title and raise a warning if an unsafe interpolated variable is found
2626
def check_unsafe_title(title)
2727
title.each do |token|
28-
notify_warning(token.next_code_token) if interpolated?(token)
28+
notify_warning(token.next_code_token) if unsafe_interpolated?(token)
2929
end
3030
end
3131

3232
# Iterates over an exec resource and if a command, onlyif or unless paramter is found, it is checked for unsafe interpolations
3333
def check_unsafe_interpolations(command_resources)
3434
command_resources[:tokens].each do |token|
3535
# Skip iteration if token isn't a command of type :NAME
36-
next unless COMMANDS.include?(token.value) && token.type == :NAME
36+
next unless COMMANDS.include?(token.value) && (token.type == :NAME || token.type == :UNLESS)
3737
# Don't check the command if it is parameterised
3838
next if parameterised?(token)
3939

@@ -60,7 +60,7 @@ def check_command(token)
6060
# Iterate through tokens in command
6161
while current_token.type != :NEWLINE
6262
# Check if token is a varibale and if it is parameterised
63-
rule_violations.append(current_token.next_code_token) if interpolated?(current_token)
63+
rule_violations.append(current_token.next_code_token) if unsafe_interpolated?(current_token)
6464
current_token = current_token.next_token
6565
end
6666

@@ -78,7 +78,7 @@ def parameterised?(token)
7878
end
7979

8080
# This function is a replacement for puppet_lint's title_tokens function which assumes titles have single quotes
81-
# This function adds a check for titles in double quotes where there could be interpolated variables
81+
# This function adds a check for titles in double quotes where there could be unsafe interpolated variables
8282
def get_exec_titles
8383
result = []
8484
tokens.each_index do |token_idx|
@@ -114,8 +114,52 @@ def get_exec_titles
114114
result
115115
end
116116

117-
def interpolated?(token)
118-
INTERPOLATED_STRINGS.include?(token.type)
117+
def find_closing_brack(token)
118+
while (token = token.next_code_token)
119+
case token.type
120+
when :RBRACK
121+
return token
122+
when :LBRACK
123+
token = find_closing_brack(token)
124+
end
125+
end
126+
raise 'not reached'
127+
end
128+
129+
def find_closing_paren(token)
130+
while (token = token.next_code_token)
131+
case token.type
132+
when :RPAREN
133+
return token
134+
when :LPAREN
135+
token = find_closing_paren(token)
136+
end
137+
end
138+
raise 'not reached'
139+
end
140+
141+
def unsafe_interpolated?(token)
142+
# XXX: Since stdlib 9.0.0 'shell_escape' is deprecated in favor or 'stdlib::shell_escape'
143+
# When the shell_escape function is removed from stdlib, we want to remove it bellow.
144+
return false unless INTERPOLATED_STRINGS.include?(token.type)
145+
146+
token = token.next_code_token
147+
148+
if token.type == :FUNCTION_NAME && ['shell_escape', 'stdlib::shell_escape'].include?(token.value)
149+
token = token.next_code_token
150+
token = find_closing_paren(token).next_code_token if token.type == :LPAREN
151+
return ![:DQPOST, :DQMID].include?(token.type)
152+
elsif token.type == :VARIABLE
153+
token = token.next_code_token
154+
token = find_closing_brack(token).next_code_token if token.type == :LBRACK
155+
if token.type == :DOT && [:NAME, :FUNCTION_NAME].include?(token.next_code_token.type) && ['shell_escape', 'stdlib::shell_escape'].include?(token.next_code_token.value)
156+
token = token.next_code_token.next_code_token
157+
token = find_closing_paren(token).next_code_token if token.type == :LPAREN
158+
return ![:DQPOST, :DQMID].include?(token.type)
159+
end
160+
end
161+
162+
true
119163
end
120164

121165
# Finds the index offset of the next instance of `value` in `tokens_slice` from the original index

spec/puppet-lint/plugins/check_unsafe_interpolations_spec.rb

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,23 @@ class foo {
3333
3434
exec { 'bar':
3535
command => "echo ${foo} ${bar}",
36+
onlyif => "${baz}",
37+
unless => "${bazz}",
3638
}
3739
3840
}
3941
PUPPET
4042
end
4143

4244
it 'detects multiple unsafe exec command arguments' do
43-
expect(problems).to have(2).problems
45+
expect(problems).to have(4).problems
4446
end
4547

4648
it 'creates two warnings' do
4749
expect(problems).to contain_warning(msg)
48-
expect(problems).to contain_warning(msg)
50+
expect(problems).to contain_warning("unsafe interpolation of variable 'bar' in exec command")
51+
expect(problems).to contain_warning("unsafe interpolation of variable 'baz' in exec command")
52+
expect(problems).to contain_warning("unsafe interpolation of variable 'bazz' in exec command")
4953
end
5054
end
5155

@@ -87,6 +91,44 @@ class foo {
8791
end
8892
end
8993

94+
context 'exec with shell escaped string in command' do
95+
let(:code) do
96+
<<-PUPPET
97+
class foo {
98+
99+
exec { 'bar':
100+
command => "echo ${foo.stdlib::shell_escape} ${bar.stdlib::shell_escape()}",
101+
onlyif => "${bar[1].stdlib::shell_escape}",
102+
unless => "[ -x ${stdlib::shell_escape(reticulate($baz))} ]",
103+
}
104+
}
105+
PUPPET
106+
end
107+
108+
it 'detects zero problems' do
109+
expect(problems).to have(0).problems
110+
end
111+
end
112+
113+
context 'exec with post-processed shell escaped string in command' do
114+
let(:code) do
115+
<<-PUPPET
116+
class foo {
117+
118+
exec { 'bar':
119+
command => "echo ${reticulate(foo.stdlib::shell_escape)} ${bar.stdlib::shell_escape().reticulate}",
120+
onlyif => "${bar[1].stdlib::shell_escape.reticulate()}",
121+
unless => "[ -x ${reticulate(stdlib::shell_escape($baz))} ]",
122+
}
123+
}
124+
PUPPET
125+
end
126+
127+
it 'detects zero problems' do
128+
expect(problems).to have(4).problems
129+
end
130+
end
131+
90132
context 'exec that has an array of args in command' do
91133
let(:code) do
92134
<<-PUPPET

spec/spec_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
require 'puppet-lint'
2+
require 'rspec/collection_matchers'
23

34
PuppetLint::Plugins.load_spec_helper

0 commit comments

Comments
 (0)