Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove subprocess with shell=True #24

Merged
merged 29 commits into from
Jan 12, 2023
Merged

Remove subprocess with shell=True #24

merged 29 commits into from
Jan 12, 2023

Conversation

maipbui
Copy link
Contributor

@maipbui maipbui commented Nov 2, 2022

Signed-off-by: maipbui maibui@microsoft.com

Why I did it

subprocess is used with shell=True, which is very dangerous for shell injection.

How I did it

remove shell=True, use shell=False

How to verify it

Pass UT
Manual test

Signed-off-by: maipbui <maibui@microsoft.com>
@lgtm-com
Copy link

lgtm-com bot commented Nov 2, 2022

This pull request introduces 1 alert when merging c1251e9 into bc8698d - view on LGTM.com

new alerts:

  • 1 for Unused import

Signed-off-by: maipbui <maibui@microsoft.com>
if raise_exception:
raise

def run_cmd_pipe(cmd0, cmd1, cmd2, log_err=True, raise_exception=False):
Copy link
Contributor

@qiluo-msft qiluo-msft Nov 2, 2022

Choose a reason for hiding this comment

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

run_cmd_pipe

Similar function already implemented. Could you reuse in the scope of this repo? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

which function are you referring to?

Copy link
Contributor

Choose a reason for hiding this comment

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

You implemented twice run_commands_pipe in this PR. I understand they are different a little bit. Could you try unify and reuse?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unified duplicate code to one function.

scripts/hostcfgd Outdated
cmd = "sed -e {0} {1} > {1}.new; mv -f {1} {1}.old; mv -f {1}.new {1}".format(' -e '.join(operations), filename)
os.system(cmd)
e_operations_str = ' -e '.join(operations)
e_operations_list = split(e_operations_str)
Copy link
Contributor

@qiluo-msft qiluo-msft Nov 2, 2022

Choose a reason for hiding this comment

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

e_operations_list

operations is already a list, no need to join and then split. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the list is joined by ' -e ', not whitespace?

Copy link
Contributor

Choose a reason for hiding this comment

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

I got it. Better to implement the transformation on a list directly.

scripts/hostcfgd Outdated
os.system(cmd)
e_operations_str = ' -e '.join(operations)
e_operations_list = split(e_operations_str)
with open(filename+'new', 'w') as f:
Copy link
Contributor

@qiluo-msft qiluo-msft Nov 2, 2022

Choose a reason for hiding this comment

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

new

you miss . ? #Closed

scripts/hostcfgd Outdated
@@ -971,9 +987,11 @@ class PasswHardening(object):

def modify_single_file_inplace(self, filename, operations=None):
if operations:
cmd = "sed -i {0} {1}".format(' -i '.join(operations), filename)
i_operations_str = ' -i '.join(operations)
i_operations_list = split(i_operations_str)
Copy link
Contributor

@qiluo-msft qiluo-msft Nov 2, 2022

Choose a reason for hiding this comment

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

split

The same. #Closed

cmd0 = ["ps", "-eo uid,pid,ppid,%mem,%cpu,stime,tty,time,cmd", "--sort", "-%cpu"]
cmd1 = ["head", "-1024"]
exitcode, data = getstatusoutput_noshell_pipe(cmd0, cmd1)
if exitcode != [0, 0]:
Copy link
Contributor

@qiluo-msft qiluo-msft Nov 2, 2022

Choose a reason for hiding this comment

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

exitcode

If you want to check all array elements are zero, you can use not exitcode.any() #Closed

Signed-off-by: maipbui <maibui@microsoft.com>
@@ -44,6 +44,8 @@ def test_caclmgrd_bfd(self, test_name, test_data, fs):
call_rc = test_data["call_rc"]
mocked_subprocess.call.return_value = call_rc

self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ip = mock.MagicMock()
self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ipv6 = mock.MagicMock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do you add mock? And why can original unit test work without mock?

Copy link
Contributor Author

@maipbui maipbui Nov 3, 2022

Choose a reason for hiding this comment

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

Without mock, it throws this error:

scripts/caclmgrd:202: in run_commands_pipe
    exitcodes, stdout = getstatusoutput_noshell_pipe(cmd0, cmd1, cmd2, cmd3)
/usr/local/lib/python3.9/dist-packages/sonic_py_common/general.py:52: in getstatusoutput_noshell_pipe
    popens = [Popen(cmd0, stdout=PIPE, universal_newlines=True)]
[OSError]: [Error 9] Bad file descriptor in the fake filesystem: '11'

I think it is due to

If close_fds is true, all file descriptors except 0, 1 and 2 will be closed before the child process is executed. Otherwise when close_fds is false, file descriptors obey their inheritable flag as described in Inheritance of File Descriptors.

in https://docs.python.org/3/library/subprocess.html#popen-constructor
Not sure how original UT works w/o mock

Copy link
Collaborator

Choose a reason for hiding this comment

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

You have changed the behavior of this unit test, and we might have better solution.
https://github.com/sonic-net/sonic-host-services/blob/master/tests/caclmgrd/caclmgrd_bfd_test.py#L37
Above line doesn't work with your commit, and I think you should fix this mock and keep the original behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I fixed the mock and revert to original behavior. Could you help review again this UT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Other file is still using mock for get_namespace_mgmt_ip and get_namespace_mgmt_ipv6

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. And thanks so much for your suggestion!

scripts/caclmgrd Outdated
@@ -178,7 +182,7 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
commands: List of strings, each string is a shell command
Copy link
Collaborator

Choose a reason for hiding this comment

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

It should be List of List?

scripts/caclmgrd Outdated
"""
Given a list of shell commands, run them in order
Args:
commands: List of strings, each string is a shell command
Copy link
Collaborator

Choose a reason for hiding this comment

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

Still List of strings?

Signed-off-by: maipbui <maibui@microsoft.com>
Comment on lines +91 to +99
subprocess.check_call(cmd)
except Exception as err:
if log_err:
syslog.syslog(syslog.LOG_ERR, "{} - failed: return code - {}, output:\n{}"
.format(err.cmd, err.returncode, err.output))
if raise_exception:
raise

def run_cmd_pipe(cmd0, cmd1, cmd2, log_err=True, raise_exception=False):
Copy link
Contributor

Choose a reason for hiding this comment

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

Why use that code and not use the python 'sh' library?
It supports everything you need here: shell lexicography and pipe.
It is just much easier to use it.
Link to docs

Copy link
Contributor

Choose a reason for hiding this comment

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

any updates?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We want to have a more secure codebase and aim to fix potentially dangerous function calls that could introduce security vulnerabilities if used incorrectly. I fixed the functions based on Semgrep recommendations (a static analysis tool to find security issues, some refs: https://semgrep.dev/docs/cheat-sheets/python-command-injection/, https://github.com/sonic-net/sonic-buildimage/actions/runs/3412786426/jobs/5678731076). Not sure if we want to use third-party library and the 'sh' library docs does not mention anything about security.

Signed-off-by: maipbui <maibui@microsoft.com>
@ganglyu ganglyu self-requested a review November 7, 2022 03:37
@@ -44,6 +44,8 @@ def test_caclmgrd_dhcp(self, test_name, test_data, fs):

mark = test_data["mark"]

self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ip = mock.MagicMock()
self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ipv6 = mock.MagicMock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

same issue

@@ -44,6 +44,8 @@ def test_feature_present(self, test_name, test_data, fs):
call_rc = test_data["call_rc"]
mocked_subprocess.call.return_value = call_rc

self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ip = mock.MagicMock()
self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ipv6 = mock.MagicMock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

same issue

Signed-off-by: maipbui <maibui@microsoft.com>
Signed-off-by: maipbui <maibui@microsoft.com>
Signed-off-by: maipbui <maibui@microsoft.com>
ipv6_address_cmd3 = ['head', '-1']
return self.run_commands_pipe(ipv6_address_cmd0, ipv6_address_cmd1, ipv6_address_cmd2, ipv6_address_cmd3)

def log_output(self, cmd, exitcodes, stdout):
Copy link
Contributor

@qiluo-msft qiluo-msft Jan 4, 2023

Choose a reason for hiding this comment

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

log_output

Add an explicit return None at the function end, since you are expecting the behavior. #Closed

Signed-off-by: maipbui <maibui@microsoft.com>
@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Jan 4, 2023

CLA Signed

The committers listed above are authorized under a signed CLA.

Signed-off-by: maipbui <maibui@microsoft.com>
Signed-off-by: maipbui <maibui@microsoft.com>
@@ -135,7 +136,13 @@ class ProcDockerStats(daemon_base.DaemonBase):
return True

def update_processstats_command(self):
data = self.run_command("ps -eo uid,pid,ppid,%mem,%cpu,stime,tty,time,cmd --sort -%cpu | head -1024")
cmd = "ps -eo uid,pid,ppid,%mem,%cpu,stime,tty,time,cmd --sort -%cpu | head -1024"
Copy link
Contributor

@qiluo-msft qiluo-msft Jan 4, 2023

Choose a reason for hiding this comment

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

cmd = "ps -eo uid,pid,ppid,%mem,%cpu,stime,tty,time,cmd --sort -%cpu | head -1024"

Assume someone fix cmd0, but forget to change cmd, the log_error message will be wrong. #Closed

scripts/hostcfgd Outdated
if ret == 0:
syslog.syslog(syslog.LOG_INFO, "{} rule exists in {}".format(ip, chain))
else:
# Modify command from Check to Append
iptables_cmds.append(cmd.replace("check", "append"))
iptables_cmds.append([word if word != "--check" else "--append" for word in cmd])
Copy link
Contributor

@qiluo-msft qiluo-msft Jan 4, 2023

Choose a reason for hiding this comment

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

cmd

Strictly speaking, you changed behavior.
Suggest loop through cmd, replace in each element. #Closed

Signed-off-by: maipbui <maibui@microsoft.com>
Signed-off-by: maipbui <maibui@microsoft.com>
Signed-off-by: maipbui <maibui@microsoft.com>
Signed-off-by: maipbui <maibui@microsoft.com>
This reverts commit 77b9c77.
This reverts commit 1b0d54c.
@maipbui maipbui marked this pull request as ready for review January 12, 2023 20:23
@maipbui maipbui merged commit 409ecc3 into sonic-net:master Jan 12, 2023
@maipbui maipbui deleted the pysec branch January 12, 2023 20:46
@@ -22,3 +22,10 @@ def keys(self, db_id, pattern):

def get_all(self, db_id, key):
return MockConnector.data[key]

def delete_all_by_pattern(self, db_id, pattern):
Copy link
Contributor

Choose a reason for hiding this comment

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

delete_all_by_pattern

This mock function only work on special use case, not a general implementation. Please consider copy the logic from codebase, and maybe implement a mock delete() function.

maipbui added a commit to maipbui/sonic-host-services that referenced this pull request Jan 12, 2023
qiluo-msft pushed a commit that referenced this pull request Jan 12, 2023
maipbui added a commit to maipbui/sonic-host-services that referenced this pull request Jan 13, 2023
@StormLiangMS
Copy link

@dgsudharsan included the #34, which is newer version, I think we don't need this PR.

Sign up for free to 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.

6 participants