Skip to content

Commit e51a7ef

Browse files
authored
fix and unify shell escaping for user/group/directory #453 (#459)
* fix and unify shell escaping for user/group/directory * avoid duplicate calls to env methods * Improve CHANGELOG description
1 parent a703af2 commit e51a7ef

File tree

4 files changed

+40
-19
lines changed

4 files changed

+40
-19
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ appear at the top.
66
## [Unreleased][]
77

88
* Your contribution here!
9-
* [#455](https://github.com/capistrano/sshkit/pull/455): Ensure UUID of commands are stable in logging - [@lazyatom](https://github.com/lazyatom)
9+
* [#455](https://github.com/capistrano/sshkit/pull/455): Ensure UUID of commands are stable in logging - [@lazyatom](https://github.com/lazyatom)
10+
* [#453](https://github.com/capistrano/sshkit/pull/453): `as` and `within` now properly escape their user/group/path arguments, and the command nested within an `as` block is now properly escaped before passing to `sh -c`. In the unlikely case that you were manually escaping commands passed to SSHKit as a workaround, you will no longer need to do this. See [#458](https://github.com/capistrano/sshkit/issues/458) for examples of what has been fixed. - [@grosser](https://github.com/grosser)
1011

1112
## [1.18.2][] (2019-02-03)
1213

lib/sshkit/backends/abstract.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'shellwords'
2+
13
module SSHKit
24

35
module Backend
@@ -81,12 +83,12 @@ def execute(*args)
8183
def within(directory, &_block)
8284
(@pwd ||= []).push directory.to_s
8385
execute <<-EOTEST, verbosity: Logger::DEBUG
84-
if test ! -d #{File.join(@pwd)}
85-
then echo "Directory does not exist '#{File.join(@pwd)}'" 1>&2
86+
if test ! -d #{File.join(@pwd).shellescape}
87+
then echo "Directory does not exist '#{File.join(@pwd).shellescape}'" 1>&2
8688
false
8789
fi
88-
EOTEST
89-
yield
90+
EOTEST
91+
yield
9092
ensure
9193
@pwd.pop
9294
end
@@ -108,8 +110,8 @@ def as(who, &_block)
108110
@group = nil
109111
end
110112
execute <<-EOTEST, verbosity: Logger::DEBUG
111-
if ! sudo -u #{@user} whoami > /dev/null
112-
then echo "You cannot switch to user '#{@user}' using sudo, please check the sudoers file" 1>&2
113+
if ! sudo -u #{@user.to_s.shellescape} whoami > /dev/null
114+
then echo "You cannot switch to user '#{@user.to_s.shellescape}' using sudo, please check the sudoers file" 1>&2
113115
false
114116
fi
115117
EOTEST

lib/sshkit/command.rb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'digest/sha1'
22
require 'securerandom'
3+
require 'shellwords'
34

45
# @author Lee Hambley
56
module SSHKit
@@ -142,7 +143,7 @@ def should_map?
142143

143144
def within(&_block)
144145
return yield unless options[:in]
145-
"cd #{options[:in]} && #{yield}"
146+
"cd #{options[:in].shellescape} && #{yield}"
146147
end
147148

148149
def environment_hash
@@ -158,13 +159,15 @@ def environment_string
158159
end
159160

160161
def with(&_block)
161-
return yield unless environment_hash.any?
162-
"( export #{environment_string} ; #{yield} )"
162+
env_string = environment_string
163+
return yield if env_string.empty?
164+
"( export #{env_string} ; #{yield} )"
163165
end
164166

165167
def user(&_block)
166168
return yield unless options[:user]
167-
"sudo -u #{options[:user]} #{environment_string + " " unless environment_string.empty?}-- sh -c '#{yield}'"
169+
env_string = environment_string
170+
"sudo -u #{options[:user].to_s.shellescape} #{env_string + " " unless env_string.empty?}-- sh -c #{yield.shellescape}"
168171
end
169172

170173
def in_background(&_block)
@@ -179,7 +182,7 @@ def umask(&_block)
179182

180183
def group(&_block)
181184
return yield unless options[:group]
182-
%Q(sg #{options[:group]} -c "#{yield}")
185+
"sg #{options[:group].to_s.shellescape} -c #{yield.shellescape}"
183186
# We could also use the so-called heredoc format perhaps:
184187
#"newgrp #{options[:group]} <<EOC \\\"%s\\\" EOC" % %Q{#{yield}}
185188
end

test/unit/test_command.rb

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ def test_including_the_env_with_string_keys
5151
def test_double_quotes_are_escaped_in_env
5252
SSHKit.config = nil
5353
c = Command.new(:rails, 'server', env: {foo: 'asdf"hjkl'})
54-
assert_equal %{( export FOO="asdf\\\"hjkl" ; /usr/bin/env rails server )}, c.to_command
54+
assert_equal %{( export FOO="asdf\\"hjkl" ; /usr/bin/env rails server )}, c.to_command
5555
end
5656

5757
def test_percentage_symbol_handled_in_env
5858
SSHKit.config = nil
5959
c = Command.new(:rails, 'server', env: {foo: 'asdf%hjkl'}, user: "anotheruser")
60-
assert_equal %{( export FOO="asdf%hjkl" ; sudo -u anotheruser FOO=\"asdf%hjkl\" -- sh -c '/usr/bin/env rails server' )}, c.to_command
60+
assert_equal %{( export FOO="asdf%hjkl" ; sudo -u anotheruser FOO=\"asdf%hjkl\" -- sh -c /usr/bin/env\\ rails\\ server )}, c.to_command
6161
end
6262

6363
def test_including_the_env_doesnt_addressively_escape
@@ -84,6 +84,11 @@ def test_working_in_a_given_directory
8484
assert_equal "cd /opt/sites && /usr/bin/env ls -l", c.to_command
8585
end
8686

87+
def test_working_in_a_given_weird_directory
88+
c = Command.new(:ls, '-l', in: "/opt/sites and stuff")
89+
assert_equal "cd /opt/sites\\ and\\ stuff && /usr/bin/env ls -l", c.to_command
90+
end
91+
8792
def test_working_in_a_given_directory_with_env
8893
c = Command.new(:ls, '-l', in: "/opt/sites", env: {a: :b})
8994
assert_equal %{cd /opt/sites && ( export A="b" ; /usr/bin/env ls -l )}, c.to_command
@@ -97,17 +102,27 @@ def test_having_a_host_passed
97102

98103
def test_working_as_a_given_user
99104
c = Command.new(:whoami, user: :anotheruser)
100-
assert_equal "sudo -u anotheruser -- sh -c '/usr/bin/env whoami'", c.to_command
105+
assert_equal "sudo -u anotheruser -- sh -c /usr/bin/env\\ whoami", c.to_command
106+
end
107+
108+
def test_working_as_a_given_weird_user
109+
c = Command.new(:whoami, user: "mr space |")
110+
assert_equal "sudo -u mr\\ space\\ \\| -- sh -c /usr/bin/env\\ whoami", c.to_command
101111
end
102112

103113
def test_working_as_a_given_group
104114
c = Command.new(:whoami, group: :devvers)
105-
assert_equal 'sg devvers -c "/usr/bin/env whoami"', c.to_command
115+
assert_equal 'sg devvers -c /usr/bin/env\\ whoami', c.to_command
116+
end
117+
118+
def test_working_as_a_given_weird_group
119+
c = Command.new(:whoami, group: "space | group")
120+
assert_equal "sg space\\ \\|\\ group -c /usr/bin/env\\ whoami", c.to_command
106121
end
107122

108123
def test_working_as_a_given_user_and_group
109124
c = Command.new(:whoami, user: :anotheruser, group: :devvers)
110-
assert_equal %Q(sudo -u anotheruser -- sh -c 'sg devvers -c "/usr/bin/env whoami"'), c.to_command
125+
assert_equal %Q(sudo -u anotheruser -- sh -c sg\\ devvers\\ -c\\ /usr/bin/env\\\\\\ whoami), c.to_command
111126
end
112127

113128
def test_umask
@@ -125,13 +140,13 @@ def test_umask_with_working_directory
125140
def test_umask_with_working_directory_and_user
126141
SSHKit.config.umask = '007'
127142
c = Command.new(:touch, 'somefile', in: '/var', user: 'alice')
128-
assert_equal "cd /var && umask 007 && sudo -u alice -- sh -c '/usr/bin/env touch somefile'", c.to_command
143+
assert_equal "cd /var && umask 007 && sudo -u alice -- sh -c /usr/bin/env\\ touch\\ somefile", c.to_command
129144
end
130145

131146
def test_umask_with_env_and_working_directory_and_user
132147
SSHKit.config.umask = '007'
133148
c = Command.new(:touch, 'somefile', user: 'bob', env: {a: 'b'}, in: '/var')
134-
assert_equal %{cd /var && umask 007 && ( export A="b" ; sudo -u bob A="b" -- sh -c '/usr/bin/env touch somefile' )}, c.to_command
149+
assert_equal %{cd /var && umask 007 && ( export A="b" ; sudo -u bob A="b" -- sh -c /usr/bin/env\\ touch\\ somefile )}, c.to_command
135150
end
136151

137152
def test_verbosity_defaults_to_logger_info

0 commit comments

Comments
 (0)