Skip to content

Safely invoke commands over SSH - #18

Merged
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh
Nov 15, 2017
Merged

Safely invoke commands over SSH#18
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh

Conversation

@martyone

Copy link
Copy Markdown
Contributor

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Used GitHub's review interface to post comments to the lines changed, hopefully it will handle this well)

I noticed in the alternative PR #20 that this has been already dealt with by quoting arguments that were observed to contain spaces in past. Note that this broke the scenario where the same arguments were used with the run() function instead of ssh() (seems no one has been using it this way for couple of months at least). Fixing that I also noticed the same issue duplicated in tester.py, so extended the commit.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how the original arguments differ from the invocation of run() couple of lines above. Now ssh() and run() can be used equally just like popen with list of string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/worker.py
elif self.ict == "mic":
mic_comm.append('%s' % job_args.image_type)
mic_comm.append('"%s"' % job_args.ksfile_name)
mic_comm.append('%s' % job_args.ksfile_name)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how mic_comm is passed either to ssh() or run() below, based on some condition. The extra quotes fixes the original ssh() invocation but otoh would break the run() invocation.


if " " in tokenvalue:
tokenvalue = '"%s"' % tokenvalue

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this would broke the local execution as commented before.

@larstiq

Copy link
Copy Markdown
Contributor

I noticed in the alternative PR #20 that this has been already dealt with by
quoting arguments that were observed to contain spaces in past. Note that this
broke the scenario where the same arguments were used with the run() function
instead of ssh() (seems no one has been using it this way for couple of months
at least). Fixing that I also noticed the same issue duplicated in tester.py,
so extended the commit.

Right, I never considered the run() case. If they were/are indeed meant to be
used with the same api then some of the calling code was broken, as well as the
ssh implementation. Fixing them like this makes sense to me, so let's
continue with this PR making sure we catch all issues.

@larstiqlarstiq left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:

- ImageTest.update_vm invokes
addrepo_comm.extend([reponame, '%s' % repo])
self.commands.ssh(addrepo_comm)
- ImageTester.install_tests invokes
addrepo_comm.extend(['testtools', '%s' % self.testtools_repourl])
self.commands.ssh(addrepo_comm)
- ssh() calls run() calls self.run(kill_comm), kill_comm being `pkill -f "
".join(command)`, does this all keep working correctly?
- are there other parts of mic_comm that are pre-escaped?
In theory we can get old data when a job is resubmitted, not sure
we should worry about that.

I'd also argue we should test this heavily, this change has more than usual
risk of breaking image builds.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we start caring about Python3 compatibility? I.e:

try:
from pipes import quote
except ImportError:
# Moved in Python3.3
from shlex import quote

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:
[...]
addrepo_comm.extend([reponame, '%s' % repo])

I would say these are equal to pass the variable directly and the only reason why the percent substitution is used is to keep similar code visually aligned.

  • ssh() calls run() calls self.run(kill_comm), kill_comm being pkill -f " ".join(command), does this all keep working correctly?

Yes IIUC it will work equally.

BTW, thinking why is this pkill required at all when it is followed by proc.terminate() which sends SIGTERM to the process selected by stored PID. It seems it is only needed to properly kill qemu-kvm which is executed with '-daemonize' (forking again). Does the '-daemonize' option have any other effect for which it is needed? If not then omitting the '-daemonize' option should make the pkill useless.

  • are there other parts of mic_comm that are pre-escaped?

     In theory we can get old data when a job is resubmitted, not sure
    we should worry about that.
    

I'd also argue we should test this heavily, this change has more than usual risk of breaking image builds.

This change is currently needed just for SDK builds, i.e., reverting this change only affects SDK builds. So unless it takes too much time to (re)deploy imager I suggest to simply test this in production environment.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

@xfade
xfade merged commit 7117948 into MeeGoIntegration:masterNov 15, 2017
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@martyone@larstiq@lbt@mkosola@xfade
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Safely invoke commands over SSH by martyone · Pull Request #18 · MeeGoIntegration/imager · GitHub
Skip to content

Safely invoke commands over SSH - #18

Merged
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh
Nov 15, 2017
Merged

Safely invoke commands over SSH#18
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh

Conversation

@martyone

Copy link
Copy Markdown
Contributor

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Used GitHub's review interface to post comments to the lines changed, hopefully it will handle this well)

I noticed in the alternative PR #20 that this has been already dealt with by quoting arguments that were observed to contain spaces in past. Note that this broke the scenario where the same arguments were used with the run() function instead of ssh() (seems no one has been using it this way for couple of months at least). Fixing that I also noticed the same issue duplicated in tester.py, so extended the commit.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how the original arguments differ from the invocation of run() couple of lines above. Now ssh() and run() can be used equally just like popen with list of string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/worker.py
elif self.ict == "mic":
mic_comm.append('%s' % job_args.image_type)
mic_comm.append('"%s"' % job_args.ksfile_name)
mic_comm.append('%s' % job_args.ksfile_name)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how mic_comm is passed either to ssh() or run() below, based on some condition. The extra quotes fixes the original ssh() invocation but otoh would break the run() invocation.


if " " in tokenvalue:
tokenvalue = '"%s"' % tokenvalue

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this would broke the local execution as commented before.

@larstiq

Copy link
Copy Markdown
Contributor

I noticed in the alternative PR #20 that this has been already dealt with by
quoting arguments that were observed to contain spaces in past. Note that this
broke the scenario where the same arguments were used with the run() function
instead of ssh() (seems no one has been using it this way for couple of months
at least). Fixing that I also noticed the same issue duplicated in tester.py,
so extended the commit.

Right, I never considered the run() case. If they were/are indeed meant to be
used with the same api then some of the calling code was broken, as well as the
ssh implementation. Fixing them like this makes sense to me, so let's
continue with this PR making sure we catch all issues.

@larstiqlarstiq left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:

- ImageTest.update_vm invokes
addrepo_comm.extend([reponame, '%s' % repo])
self.commands.ssh(addrepo_comm)
- ImageTester.install_tests invokes
addrepo_comm.extend(['testtools', '%s' % self.testtools_repourl])
self.commands.ssh(addrepo_comm)
- ssh() calls run() calls self.run(kill_comm), kill_comm being `pkill -f "
".join(command)`, does this all keep working correctly?
- are there other parts of mic_comm that are pre-escaped?
In theory we can get old data when a job is resubmitted, not sure
we should worry about that.

I'd also argue we should test this heavily, this change has more than usual
risk of breaking image builds.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we start caring about Python3 compatibility? I.e:

try:
from pipes import quote
except ImportError:
# Moved in Python3.3
from shlex import quote

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:
[...]
addrepo_comm.extend([reponame, '%s' % repo])

I would say these are equal to pass the variable directly and the only reason why the percent substitution is used is to keep similar code visually aligned.

  • ssh() calls run() calls self.run(kill_comm), kill_comm being pkill -f " ".join(command), does this all keep working correctly?

Yes IIUC it will work equally.

BTW, thinking why is this pkill required at all when it is followed by proc.terminate() which sends SIGTERM to the process selected by stored PID. It seems it is only needed to properly kill qemu-kvm which is executed with '-daemonize' (forking again). Does the '-daemonize' option have any other effect for which it is needed? If not then omitting the '-daemonize' option should make the pkill useless.

  • are there other parts of mic_comm that are pre-escaped?

     In theory we can get old data when a job is resubmitted, not sure
    we should worry about that.
    

I'd also argue we should test this heavily, this change has more than usual risk of breaking image builds.

This change is currently needed just for SDK builds, i.e., reverting this change only affects SDK builds. So unless it takes too much time to (re)deploy imager I suggest to simply test this in production environment.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

@xfade
xfade merged commit 7117948 into MeeGoIntegration:masterNov 15, 2017
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@martyone@larstiq@lbt@mkosola@xfade
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Safely invoke commands over SSH by martyone · Pull Request #18 · MeeGoIntegration/imager · GitHub
Skip to content

Safely invoke commands over SSH - #18

Merged
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh
Nov 15, 2017
Merged

Safely invoke commands over SSH#18
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh

Conversation

@martyone

Copy link
Copy Markdown
Contributor

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Used GitHub's review interface to post comments to the lines changed, hopefully it will handle this well)

I noticed in the alternative PR #20 that this has been already dealt with by quoting arguments that were observed to contain spaces in past. Note that this broke the scenario where the same arguments were used with the run() function instead of ssh() (seems no one has been using it this way for couple of months at least). Fixing that I also noticed the same issue duplicated in tester.py, so extended the commit.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how the original arguments differ from the invocation of run() couple of lines above. Now ssh() and run() can be used equally just like popen with list of string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/worker.py
elif self.ict == "mic":
mic_comm.append('%s' % job_args.image_type)
mic_comm.append('"%s"' % job_args.ksfile_name)
mic_comm.append('%s' % job_args.ksfile_name)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how mic_comm is passed either to ssh() or run() below, based on some condition. The extra quotes fixes the original ssh() invocation but otoh would break the run() invocation.


if " " in tokenvalue:
tokenvalue = '"%s"' % tokenvalue

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this would broke the local execution as commented before.

@larstiq

Copy link
Copy Markdown
Contributor

I noticed in the alternative PR #20 that this has been already dealt with by
quoting arguments that were observed to contain spaces in past. Note that this
broke the scenario where the same arguments were used with the run() function
instead of ssh() (seems no one has been using it this way for couple of months
at least). Fixing that I also noticed the same issue duplicated in tester.py,
so extended the commit.

Right, I never considered the run() case. If they were/are indeed meant to be
used with the same api then some of the calling code was broken, as well as the
ssh implementation. Fixing them like this makes sense to me, so let's
continue with this PR making sure we catch all issues.

@larstiqlarstiq left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:

- ImageTest.update_vm invokes
addrepo_comm.extend([reponame, '%s' % repo])
self.commands.ssh(addrepo_comm)
- ImageTester.install_tests invokes
addrepo_comm.extend(['testtools', '%s' % self.testtools_repourl])
self.commands.ssh(addrepo_comm)
- ssh() calls run() calls self.run(kill_comm), kill_comm being `pkill -f "
".join(command)`, does this all keep working correctly?
- are there other parts of mic_comm that are pre-escaped?
In theory we can get old data when a job is resubmitted, not sure
we should worry about that.

I'd also argue we should test this heavily, this change has more than usual
risk of breaking image builds.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we start caring about Python3 compatibility? I.e:

try:
from pipes import quote
except ImportError:
# Moved in Python3.3
from shlex import quote

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:
[...]
addrepo_comm.extend([reponame, '%s' % repo])

I would say these are equal to pass the variable directly and the only reason why the percent substitution is used is to keep similar code visually aligned.

  • ssh() calls run() calls self.run(kill_comm), kill_comm being pkill -f " ".join(command), does this all keep working correctly?

Yes IIUC it will work equally.

BTW, thinking why is this pkill required at all when it is followed by proc.terminate() which sends SIGTERM to the process selected by stored PID. It seems it is only needed to properly kill qemu-kvm which is executed with '-daemonize' (forking again). Does the '-daemonize' option have any other effect for which it is needed? If not then omitting the '-daemonize' option should make the pkill useless.

  • are there other parts of mic_comm that are pre-escaped?

     In theory we can get old data when a job is resubmitted, not sure
    we should worry about that.
    

I'd also argue we should test this heavily, this change has more than usual risk of breaking image builds.

This change is currently needed just for SDK builds, i.e., reverting this change only affects SDK builds. So unless it takes too much time to (re)deploy imager I suggest to simply test this in production environment.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

@xfade
xfade merged commit 7117948 into MeeGoIntegration:masterNov 15, 2017
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@martyone@larstiq@lbt@mkosola@xfade
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Safely invoke commands over SSH by martyone · Pull Request #18 · MeeGoIntegration/imager · GitHub
Skip to content

Safely invoke commands over SSH - #18

Merged
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh
Nov 15, 2017
Merged

Safely invoke commands over SSH#18
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh

Conversation

@martyone

Copy link
Copy Markdown
Contributor

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Used GitHub's review interface to post comments to the lines changed, hopefully it will handle this well)

I noticed in the alternative PR #20 that this has been already dealt with by quoting arguments that were observed to contain spaces in past. Note that this broke the scenario where the same arguments were used with the run() function instead of ssh() (seems no one has been using it this way for couple of months at least). Fixing that I also noticed the same issue duplicated in tester.py, so extended the commit.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how the original arguments differ from the invocation of run() couple of lines above. Now ssh() and run() can be used equally just like popen with list of string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/worker.py
elif self.ict == "mic":
mic_comm.append('%s' % job_args.image_type)
mic_comm.append('"%s"' % job_args.ksfile_name)
mic_comm.append('%s' % job_args.ksfile_name)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how mic_comm is passed either to ssh() or run() below, based on some condition. The extra quotes fixes the original ssh() invocation but otoh would break the run() invocation.


if " " in tokenvalue:
tokenvalue = '"%s"' % tokenvalue

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this would broke the local execution as commented before.

@larstiq

Copy link
Copy Markdown
Contributor

I noticed in the alternative PR #20 that this has been already dealt with by
quoting arguments that were observed to contain spaces in past. Note that this
broke the scenario where the same arguments were used with the run() function
instead of ssh() (seems no one has been using it this way for couple of months
at least). Fixing that I also noticed the same issue duplicated in tester.py,
so extended the commit.

Right, I never considered the run() case. If they were/are indeed meant to be
used with the same api then some of the calling code was broken, as well as the
ssh implementation. Fixing them like this makes sense to me, so let's
continue with this PR making sure we catch all issues.

@larstiqlarstiq left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:

- ImageTest.update_vm invokes
addrepo_comm.extend([reponame, '%s' % repo])
self.commands.ssh(addrepo_comm)
- ImageTester.install_tests invokes
addrepo_comm.extend(['testtools', '%s' % self.testtools_repourl])
self.commands.ssh(addrepo_comm)
- ssh() calls run() calls self.run(kill_comm), kill_comm being `pkill -f "
".join(command)`, does this all keep working correctly?
- are there other parts of mic_comm that are pre-escaped?
In theory we can get old data when a job is resubmitted, not sure
we should worry about that.

I'd also argue we should test this heavily, this change has more than usual
risk of breaking image builds.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we start caring about Python3 compatibility? I.e:

try:
from pipes import quote
except ImportError:
# Moved in Python3.3
from shlex import quote

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:
[...]
addrepo_comm.extend([reponame, '%s' % repo])

I would say these are equal to pass the variable directly and the only reason why the percent substitution is used is to keep similar code visually aligned.

  • ssh() calls run() calls self.run(kill_comm), kill_comm being pkill -f " ".join(command), does this all keep working correctly?

Yes IIUC it will work equally.

BTW, thinking why is this pkill required at all when it is followed by proc.terminate() which sends SIGTERM to the process selected by stored PID. It seems it is only needed to properly kill qemu-kvm which is executed with '-daemonize' (forking again). Does the '-daemonize' option have any other effect for which it is needed? If not then omitting the '-daemonize' option should make the pkill useless.

  • are there other parts of mic_comm that are pre-escaped?

     In theory we can get old data when a job is resubmitted, not sure
    we should worry about that.
    

I'd also argue we should test this heavily, this change has more than usual risk of breaking image builds.

This change is currently needed just for SDK builds, i.e., reverting this change only affects SDK builds. So unless it takes too much time to (re)deploy imager I suggest to simply test this in production environment.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

@xfade
xfade merged commit 7117948 into MeeGoIntegration:masterNov 15, 2017
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@martyone@larstiq@lbt@mkosola@xfade
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Safely invoke commands over SSH by martyone · Pull Request #18 · MeeGoIntegration/imager · GitHub
Skip to content

Safely invoke commands over SSH - #18

Merged
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh
Nov 15, 2017
Merged

Safely invoke commands over SSH#18
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh

Conversation

@martyone

Copy link
Copy Markdown
Contributor

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Used GitHub's review interface to post comments to the lines changed, hopefully it will handle this well)

I noticed in the alternative PR #20 that this has been already dealt with by quoting arguments that were observed to contain spaces in past. Note that this broke the scenario where the same arguments were used with the run() function instead of ssh() (seems no one has been using it this way for couple of months at least). Fixing that I also noticed the same issue duplicated in tester.py, so extended the commit.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how the original arguments differ from the invocation of run() couple of lines above. Now ssh() and run() can be used equally just like popen with list of string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/worker.py
elif self.ict == "mic":
mic_comm.append('%s' % job_args.image_type)
mic_comm.append('"%s"' % job_args.ksfile_name)
mic_comm.append('%s' % job_args.ksfile_name)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how mic_comm is passed either to ssh() or run() below, based on some condition. The extra quotes fixes the original ssh() invocation but otoh would break the run() invocation.


if " " in tokenvalue:
tokenvalue = '"%s"' % tokenvalue

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this would broke the local execution as commented before.

@larstiq

Copy link
Copy Markdown
Contributor

I noticed in the alternative PR #20 that this has been already dealt with by
quoting arguments that were observed to contain spaces in past. Note that this
broke the scenario where the same arguments were used with the run() function
instead of ssh() (seems no one has been using it this way for couple of months
at least). Fixing that I also noticed the same issue duplicated in tester.py,
so extended the commit.

Right, I never considered the run() case. If they were/are indeed meant to be
used with the same api then some of the calling code was broken, as well as the
ssh implementation. Fixing them like this makes sense to me, so let's
continue with this PR making sure we catch all issues.

@larstiqlarstiq left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:

- ImageTest.update_vm invokes
addrepo_comm.extend([reponame, '%s' % repo])
self.commands.ssh(addrepo_comm)
- ImageTester.install_tests invokes
addrepo_comm.extend(['testtools', '%s' % self.testtools_repourl])
self.commands.ssh(addrepo_comm)
- ssh() calls run() calls self.run(kill_comm), kill_comm being `pkill -f "
".join(command)`, does this all keep working correctly?
- are there other parts of mic_comm that are pre-escaped?
In theory we can get old data when a job is resubmitted, not sure
we should worry about that.

I'd also argue we should test this heavily, this change has more than usual
risk of breaking image builds.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we start caring about Python3 compatibility? I.e:

try:
from pipes import quote
except ImportError:
# Moved in Python3.3
from shlex import quote

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:
[...]
addrepo_comm.extend([reponame, '%s' % repo])

I would say these are equal to pass the variable directly and the only reason why the percent substitution is used is to keep similar code visually aligned.

  • ssh() calls run() calls self.run(kill_comm), kill_comm being pkill -f " ".join(command), does this all keep working correctly?

Yes IIUC it will work equally.

BTW, thinking why is this pkill required at all when it is followed by proc.terminate() which sends SIGTERM to the process selected by stored PID. It seems it is only needed to properly kill qemu-kvm which is executed with '-daemonize' (forking again). Does the '-daemonize' option have any other effect for which it is needed? If not then omitting the '-daemonize' option should make the pkill useless.

  • are there other parts of mic_comm that are pre-escaped?

     In theory we can get old data when a job is resubmitted, not sure
    we should worry about that.
    

I'd also argue we should test this heavily, this change has more than usual risk of breaking image builds.

This change is currently needed just for SDK builds, i.e., reverting this change only affects SDK builds. So unless it takes too much time to (re)deploy imager I suggest to simply test this in production environment.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

@xfade
xfade merged commit 7117948 into MeeGoIntegration:masterNov 15, 2017
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@martyone@larstiq@lbt@mkosola@xfade
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Safely invoke commands over SSH by martyone · Pull Request #18 · MeeGoIntegration/imager · GitHub
Skip to content

Safely invoke commands over SSH - #18

Merged
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh
Nov 15, 2017
Merged

Safely invoke commands over SSH#18
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh

Conversation

@martyone

Copy link
Copy Markdown
Contributor

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Used GitHub's review interface to post comments to the lines changed, hopefully it will handle this well)

I noticed in the alternative PR #20 that this has been already dealt with by quoting arguments that were observed to contain spaces in past. Note that this broke the scenario where the same arguments were used with the run() function instead of ssh() (seems no one has been using it this way for couple of months at least). Fixing that I also noticed the same issue duplicated in tester.py, so extended the commit.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how the original arguments differ from the invocation of run() couple of lines above. Now ssh() and run() can be used equally just like popen with list of string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/worker.py
elif self.ict == "mic":
mic_comm.append('%s' % job_args.image_type)
mic_comm.append('"%s"' % job_args.ksfile_name)
mic_comm.append('%s' % job_args.ksfile_name)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how mic_comm is passed either to ssh() or run() below, based on some condition. The extra quotes fixes the original ssh() invocation but otoh would break the run() invocation.


if " " in tokenvalue:
tokenvalue = '"%s"' % tokenvalue

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this would broke the local execution as commented before.

@larstiq

Copy link
Copy Markdown
Contributor

I noticed in the alternative PR #20 that this has been already dealt with by
quoting arguments that were observed to contain spaces in past. Note that this
broke the scenario where the same arguments were used with the run() function
instead of ssh() (seems no one has been using it this way for couple of months
at least). Fixing that I also noticed the same issue duplicated in tester.py,
so extended the commit.

Right, I never considered the run() case. If they were/are indeed meant to be
used with the same api then some of the calling code was broken, as well as the
ssh implementation. Fixing them like this makes sense to me, so let's
continue with this PR making sure we catch all issues.

@larstiqlarstiq left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:

- ImageTest.update_vm invokes
addrepo_comm.extend([reponame, '%s' % repo])
self.commands.ssh(addrepo_comm)
- ImageTester.install_tests invokes
addrepo_comm.extend(['testtools', '%s' % self.testtools_repourl])
self.commands.ssh(addrepo_comm)
- ssh() calls run() calls self.run(kill_comm), kill_comm being `pkill -f "
".join(command)`, does this all keep working correctly?
- are there other parts of mic_comm that are pre-escaped?
In theory we can get old data when a job is resubmitted, not sure
we should worry about that.

I'd also argue we should test this heavily, this change has more than usual
risk of breaking image builds.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we start caring about Python3 compatibility? I.e:

try:
from pipes import quote
except ImportError:
# Moved in Python3.3
from shlex import quote

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:
[...]
addrepo_comm.extend([reponame, '%s' % repo])

I would say these are equal to pass the variable directly and the only reason why the percent substitution is used is to keep similar code visually aligned.

  • ssh() calls run() calls self.run(kill_comm), kill_comm being pkill -f " ".join(command), does this all keep working correctly?

Yes IIUC it will work equally.

BTW, thinking why is this pkill required at all when it is followed by proc.terminate() which sends SIGTERM to the process selected by stored PID. It seems it is only needed to properly kill qemu-kvm which is executed with '-daemonize' (forking again). Does the '-daemonize' option have any other effect for which it is needed? If not then omitting the '-daemonize' option should make the pkill useless.

  • are there other parts of mic_comm that are pre-escaped?

     In theory we can get old data when a job is resubmitted, not sure
    we should worry about that.
    

I'd also argue we should test this heavily, this change has more than usual risk of breaking image builds.

This change is currently needed just for SDK builds, i.e., reverting this change only affects SDK builds. So unless it takes too much time to (re)deploy imager I suggest to simply test this in production environment.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

@xfade
xfade merged commit 7117948 into MeeGoIntegration:masterNov 15, 2017
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@martyone@larstiq@lbt@mkosola@xfade
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Safely invoke commands over SSH by martyone · Pull Request #18 · MeeGoIntegration/imager · GitHub
Skip to content

Safely invoke commands over SSH - #18

Merged
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh
Nov 15, 2017
Merged

Safely invoke commands over SSH#18
xfade merged 1 commit into
MeeGoIntegration:masterfrom
martyone:safe-ssh

Conversation

@martyone

Copy link
Copy Markdown
Contributor

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

It is common misunderstanding that SSH accepts CMD [ARGS...]. Actually
it joins all positional arguments and passes the resulting single string
to sh -c.

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Used GitHub's review interface to post comments to the lines changed, hopefully it will handle this well)

I noticed in the alternative PR #20 that this has been already dealt with by quoting arguments that were observed to contain spaces in past. Note that this broke the scenario where the same arguments were used with the run() function instead of ssh() (seems no one has been using it this way for couple of months at least). Fixing that I also noticed the same issue duplicated in tester.py, so extended the commit.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how the original arguments differ from the invocation of run() couple of lines above. Now ssh() and run() can be used equally just like popen with list of string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/worker.py
elif self.ict == "mic":
mic_comm.append('%s' % job_args.image_type)
mic_comm.append('"%s"' % job_args.ksfile_name)
mic_comm.append('%s' % job_args.ksfile_name)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how mic_comm is passed either to ssh() or run() below, based on some condition. The extra quotes fixes the original ssh() invocation but otoh would break the run() invocation.


if " " in tokenvalue:
tokenvalue = '"%s"' % tokenvalue

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this would broke the local execution as commented before.

@larstiq

Copy link
Copy Markdown
Contributor

I noticed in the alternative PR #20 that this has been already dealt with by
quoting arguments that were observed to contain spaces in past. Note that this
broke the scenario where the same arguments were used with the run() function
instead of ssh() (seems no one has been using it this way for couple of months
at least). Fixing that I also noticed the same issue duplicated in tester.py,
so extended the commit.

Right, I never considered the run() case. If they were/are indeed meant to be
used with the same api then some of the calling code was broken, as well as the
ssh implementation. Fixing them like this makes sense to me, so let's
continue with this PR making sure we catch all issues.

@larstiqlarstiq left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:

- ImageTest.update_vm invokes
addrepo_comm.extend([reponame, '%s' % repo])
self.commands.ssh(addrepo_comm)
- ImageTester.install_tests invokes
addrepo_comm.extend(['testtools', '%s' % self.testtools_repourl])
self.commands.ssh(addrepo_comm)
- ssh() calls run() calls self.run(kill_comm), kill_comm being `pkill -f "
".join(command)`, does this all keep working correctly?
- are there other parts of mic_comm that are pre-escaped?
In theory we can get old data when a job is resubmitted, not sure
we should worry about that.

I'd also argue we should test this heavily, this change has more than usual
risk of breaking image builds.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we start caring about Python3 compatibility? I.e:

try:
from pipes import quote
except ImportError:
# Moved in Python3.3
from shlex import quote

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. We end up with

sh -c sh -c "'rm -rf /tmp/results/*'"

do we need a double sh -c to undo a pipes.quote level?

@martyonemartyone left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other areas to look at:
[...]
addrepo_comm.extend([reponame, '%s' % repo])

I would say these are equal to pass the variable directly and the only reason why the percent substitution is used is to keep similar code visually aligned.

  • ssh() calls run() calls self.run(kill_comm), kill_comm being pkill -f " ".join(command), does this all keep working correctly?

Yes IIUC it will work equally.

BTW, thinking why is this pkill required at all when it is followed by proc.terminate() which sends SIGTERM to the process selected by stored PID. It seems it is only needed to properly kill qemu-kvm which is executed with '-daemonize' (forking again). Does the '-daemonize' option have any other effect for which it is needed? If not then omitting the '-daemonize' option should make the pkill useless.

  • are there other parts of mic_comm that are pre-escaped?

     In theory we can get old data when a job is resubmitted, not sure
    we should worry about that.
    

I'd also argue we should test this heavily, this change has more than usual risk of breaking image builds.

This change is currently needed just for SDK builds, i.e., reverting this change only affects SDK builds. So unless it takes too much time to (re)deploy imager I suggest to simply test this in production environment.

Comment threadsrc/img/tester.py
print "trying to get any test results"
self.commands.scpfrom("/tmp/results/*", self.results_dir)
self.commands.ssh(['rm', '-rf', '/tmp/results/*'])
self.commands.ssh(['sh', '-c', 'rm -rf /tmp/results/*'])

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be

sh -c "sh -c 'rm -rf /tmp/results/*'"

We need it to get glob patterns resolved or more generally for some shell functionality.

Saying "to undo a pipes.quote level" is basically true but uncovers an implementation detail that the user of ssh() shouldn't care about. The user of ssh() needs to know that ssh() accepts a list of arguments to be directly stuffed into argv and nothing more. The user of ssh() doesn't care about unquotting; the user of ssh() sees the need for some shell functionality, so the command he executes is a shell interpreter with a shell script supplied as an argument.

This is well illistrated with the run() invocations above on lines 586 and 587 which I reffered to in my original comment. First invocation (cp -v) needs some shell functionality so it executes sh -c with the actuall command supplied in form of a shell script. Second invocation (rm -rf) has all arguments ready to be directly stuffed to argv.

Note that ideally the first invocation would have every non-literal input quoted as in

self.commands.run(['sh', '-c', "cp -v /tmp/" + pipes.quote(self.test_id) + "/results/* " + pipes.quote(self.results_dir)])

Thinking of a case when a malicious test_id is supplied by an attacker, e.g. 'foobar; rm -rf /'.

Comment threadsrc/img/tester.py
#~ along with this program. If not, see <http://www.gnu.org/licenses/>.

import os, sys
import pipes

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to port it as a whole instead (in a single step).

@xfade
xfade merged commit 7117948 into MeeGoIntegration:masterNov 15, 2017
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@martyone@larstiq@lbt@mkosola@xfade