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

[sonic-py-common] Add getstatusoutput_noshell() functions to general module #12065

Merged
merged 10 commits into from
Sep 22, 2022

Conversation

maipbui
Copy link
Contributor

@maipbui maipbui commented Sep 13, 2022

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

Why I did it

getstatusoutput() function from subprocess module has shell injection issue because it includes shell=True in the implementation
Eliminate duplicate code

How I did it

Reimplement getstatusoutput_noshell() and getstatusoutput_noshell_pipe() functions with shell=False
Add check_output_pipe() function

How to verify it

Pass UT

Which release branch to backport (provide reason below if selected)

  • 201811
  • 201911
  • 202006
  • 202012
  • 202106
  • 202111
  • 202205

Description for the changelog

Link to config_db schema for YANG module changes

A picture of a cute animal (not mandatory but encouraged)

Signed-off-by: maipbui <maibui@microsoft.com>
output = p2.communicate()[0]
p1.wait()
if p1.returncode != 0 and p2.returncode != 0:
status = 1
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 14, 2022

Choose a reason for hiding this comment

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

status = 1

This is a general function, so hide information into status 0/1 is not recommended. Suggest return an array of status as the first element in the returned pair. #Closed

@@ -23,3 +24,33 @@ def load_module_from_source(module_name, file_path):
sys.modules[module_name] = module

return module


def getstatusoutput_noshell(cmd):
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 14, 2022

Choose a reason for hiding this comment

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

getstatusoutput_noshell

Please add unit tests to cover new functions. #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.

Sure, working on UT

Signed-off-by: maipbui <maibui@microsoft.com>
with Popen(cmd2, universal_newlines=True, stdin=p1.stdout, stdout=PIPE) as p2:
output = p2.communicate()[0]
p1.wait()
if p1.returncode != 0 and p2.returncode != 0:
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 15, 2022

Choose a reason for hiding this comment

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

and

or? #Closed

with Popen(cmd2, universal_newlines=True, stdin=p1.stdout, stdout=PIPE) as p2:
output = p2.communicate()[0]
p1.wait()
if p1.returncode != 0 and p2.returncode != 0:
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 15, 2022

Choose a reason for hiding this comment

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

and

and -> or #Closed

output = p2.communicate()[0]
p1.wait()
if p1.returncode != 0 and p2.returncode != 0:
returncode = p1.returncode if p2.returncode == 0 else p2.returncode
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 15, 2022

Choose a reason for hiding this comment

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

p1

check p1 return code first, if not zero, throw based on p1 info. otherwise, check p2 return code, and throw based on p2 info. #Closed

Signed-off-by: maipbui <maibui@microsoft.com>
Signed-off-by: maipbui <maibui@microsoft.com>
output = p2.communicate()[0]
p1.wait()
if p1.returncode != 0 or p2.returncode != 0:
return ([p1.returncode, p2.returncode], output)
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 16, 2022

Choose a reason for hiding this comment

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

You do not need to specially handle this case, and fall through below code, and return [p1.returncode, p2.returncode], output #Closed

p1.wait()
if p1.returncode != 0 or p2.returncode != 0:
if p1.returncode != 0:
raise CalledProcessError(returncode=p1.returncode, cmd=cmd, output=output)
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 16, 2022

Choose a reason for hiding this comment

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

cmd

cmd1 #Closed

p1.wait()
if p1.returncode != 0 or p2.returncode != 0:
if p1.returncode != 0:
raise CalledProcessError(returncode=p1.returncode, cmd=cmd, output=output)
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 16, 2022

Choose a reason for hiding this comment

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

output

Could you use output of cmd1? #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>
@maipbui maipbui requested a review from qiluo-msft September 17, 2022 21:33
assert exitcode != 0

def test_getstatusoutput_noshell_pipes():
exitcode, output = getstatusoutput_noshell_pipes(dict(args=['echo', 'sonic']), dict(args=['awk', '{print $1}']))
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 17, 2022

Choose a reason for hiding this comment

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

Suggested change
exitcode, output = getstatusoutput_noshell_pipes(dict(args=['echo', 'sonic']), dict(args=['awk', '{print $1}']))
exitcode, output = getstatusoutput_noshell_pipes(['echo', 'sonic'], ['awk', '{print $1}'])

You can still call the function in traditional way if function parameter is *args.
#Closed

raise ValueError("Needs at least 2 processes")
# Set up more arguments in every processes
for arg in args:
arg["stdout"] = PIPE
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 17, 2022

Choose a reason for hiding this comment

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

arg["stdout"]

If caller provide an arg with stdout, you will override? Suggest not to reuse parameter var. #Closed

return (exitcodes, output)


def check_output_pipes(*args):
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 17, 2022

Choose a reason for hiding this comment

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

check_output_pipes

Maybe you can keep the name check_output_pipe #Closed

return (exitcodes, output)


def check_output_pipes(*args):
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 17, 2022

Choose a reason for hiding this comment

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

Suggested change
def check_output_pipes(*args):
def check_output_pipes(cmd0, *args):

If you will treat the first element of args differently, it's better to define it as normal parameter. I guess your implementation will also works for only 1 cmd0, then you do not need to check len(args) at all. #Closed

def check_output_pipes(*args):
"""
This function implements check_output API from subprocess module.
Input command includes two or more pipe command.
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 17, 2022

Choose a reason for hiding this comment

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

pipe command -> commands connected by shell pipe(s) #Closed

# to create the pipeline. Closes stdouts to avoid deadlocks.
# Ref: https://docs.python.org/2/library/subprocess.html#replacing-shell-pipeline
popens = [Popen(**args[0])]
for i in range(1,len(args)):
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 17, 2022

Choose a reason for hiding this comment

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

range

range -> xrange #Closed

return (exitcodes, output)


def check_output_pipes(*args):
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 17, 2022

Choose a reason for hiding this comment

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

check_output_pipes

you can still call it check_output_pipe, since one pipe is possible. Not a big deal. #Closed

maipbui referenced this pull request Oct 14, 2022
…12108)

Signed-off-by: maipbui <maibui@microsoft.com>
Dependency: [https://github.com/sonic-net/sonic-buildimage/pull/12065](https://github.com/sonic-net/sonic-buildimage/pull/12065)
#### Why I did it
1. `getstatusoutput` is used without a static string and it uses `shell=True`
2. `subprocess()` - when using with `shell=True` is dangerous. Using subprocess function without a static string can lead to command injection.
3. `os` - not secure against maliciously constructed input and dangerous if used to evaluate dynamic content.
#### How I did it
1. use `getstatusoutput` without shell=True
2. `subprocess()` - use `shell=False` instead. use an array string. Ref: [https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation](https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation)
3. `os` - use with `subprocess`
roberthong-qct pushed a commit to QuantaSwitch/sonic-buildimage that referenced this pull request Nov 18, 2022
…module (sonic-net#12065)

Signed-off-by: maipbui <maibui@microsoft.com>
#### Why I did it
`getstatusoutput()` function from `subprocess` module has shell injection issue because it includes `shell=True` in the implementation
Eliminate duplicate code
#### How I did it
Reimplement `getstatusoutput_noshell()` and `getstatusoutput_noshell_pipe()` functions with `shell=False`
Add `check_output_pipe()` function
#### How to verify it
Pass UT
maipbui referenced this pull request Nov 22, 2022
Signed-off-by: maipbui maibui@microsoft.com
Dependency: [https://github.com/sonic-net/sonic-buildimage/pull/12065](https://github.com/sonic-net/sonic-buildimage/pull/12065)
#### Why I did it
`commands` module is not secure
command injection in `getstatusoutput` being used without a static string
#### How I did it
Eliminate `commands` module, use `subprocess` module only
Convert Python 2 to Python 3
maipbui referenced this pull request Nov 28, 2022
)

Signed-off-by: maipbui <maibui@microsoft.com>
Dependency: [https://github.com/sonic-net/sonic-buildimage/pull/12065](https://github.com/sonic-net/sonic-buildimage/pull/12065)
#### Why I did it
1. `getstatusoutput` is used without a static string and it uses `shell=True`
2. `subprocess()` - when using with `shell=True` is dangerous. Using subprocess function without a static string can lead to command injection.
3. `os` - not secure against maliciously constructed input and dangerous if used to evaluate dynamic content.
#### How I did it
1. use `getstatusoutput` without shell=True
2. `subprocess()` - use `shell=False` instead. use an array string. Ref: [https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation](https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation)
3. `os` - use with `subprocess`
maipbui referenced this pull request Dec 1, 2022
Signed-off-by: maipbui <maibui@microsoft.com>
Dependency: [https://github.com/sonic-net/sonic-buildimage/pull/12065](https://github.com/sonic-net/sonic-buildimage/pull/12065)
#### Why I did it
`subprocess.Popen()` and `subprocess.run()` is used with `shell=True`, which is very dangerous for shell injection.
`os` - not secure against maliciously constructed input and dangerous if used to evaluate dynamic content
`getstatusoutput` is dangerous because it contains `shell=True` in the implementation
#### How I did it
Replace `os` by `subprocess`, use with `shell=False`
Remove unused functions
StormLiangMS referenced this pull request in StormLiangMS/sonic-buildimage Dec 8, 2022
Signed-off-by: maipbui maibui@microsoft.com
Dependency: [https://github.com/sonic-net/sonic-buildimage/pull/12065](https://github.com/sonic-net/sonic-buildimage/pull/12065)
#### Why I did it
`commands` module is not secure
command injection in `getstatusoutput` being used without a static string
#### How I did it
Eliminate `commands` module, use `subprocess` module only
Convert Python 2 to Python 3
StormLiangMS referenced this pull request in StormLiangMS/sonic-buildimage Dec 8, 2022
…ic-net#12107)

Signed-off-by: maipbui <maibui@microsoft.com>
Dependency: [https://github.com/sonic-net/sonic-buildimage/pull/12065](https://github.com/sonic-net/sonic-buildimage/pull/12065)
#### Why I did it
1. `getstatusoutput` is used without a static string and it uses `shell=True`
2. `subprocess()` - when using with `shell=True` is dangerous. Using subprocess function without a static string can lead to command injection.
3. `os` - not secure against maliciously constructed input and dangerous if used to evaluate dynamic content.
#### How I did it
1. use `getstatusoutput` without shell=True
2. `subprocess()` - use `shell=False` instead. use an array string. Ref: [https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation](https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation)
3. `os` - use with `subprocess`
maipbui added a commit that referenced this pull request Dec 9, 2022
…1740)

Signed-off-by: maipbui <maibui@microsoft.com>
Dependency: [PR (#12065)](#12065) needs to merge first.
#### Why I did it
1. `eval()` - not secure against maliciously constructed input, can be dangerous if used to evaluate dynamic content. This may be a code injection vulnerability.
2. `subprocess()` - when using with `shell=True` is dangerous. Using subprocess function without a static string can lead to command injection.
3. `os` - not secure against maliciously constructed input and dangerous if used to evaluate dynamic content.
4. `is` operator - string comparison should not be used with reference equality.
5. `globals()` - extremely dangerous because it may allow an attacker to execute arbitrary code on the system
#### How I did it
1. `eval()` - use `literal_eval()`
2. `subprocess()` - use `shell=False` instead. use an array string. Ref: [https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation](https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation)
3. `os` - use with `subprocess`
4. `is` - replace by `==` operator for value equality
5. `globals()` - avoid the use of globals()
StormLiangMS referenced this pull request Dec 10, 2022
Signed-off-by: maipbui maibui@microsoft.com
Dependency: [https://github.com/sonic-net/sonic-buildimage/pull/12065](https://github.com/sonic-net/sonic-buildimage/pull/12065)
#### Why I did it
`commands` module is not secure
command injection in `getstatusoutput` being used without a static string
#### How I did it
Eliminate `commands` module, use `subprocess` module only
Convert Python 2 to Python 3
StormLiangMS referenced this pull request Dec 10, 2022
)

Signed-off-by: maipbui <maibui@microsoft.com>
Dependency: [https://github.com/sonic-net/sonic-buildimage/pull/12065](https://github.com/sonic-net/sonic-buildimage/pull/12065)
#### Why I did it
1. `getstatusoutput` is used without a static string and it uses `shell=True`
2. `subprocess()` - when using with `shell=True` is dangerous. Using subprocess function without a static string can lead to command injection.
3. `os` - not secure against maliciously constructed input and dangerous if used to evaluate dynamic content.
#### How I did it
1. use `getstatusoutput` without shell=True
2. `subprocess()` - use `shell=False` instead. use an array string. Ref: [https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation](https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation)
3. `os` - use with `subprocess`
qiluo-msft pushed a commit that referenced this pull request Jan 6, 2023
Dependency: [PR (#12065)](#12065) needs to merge first.

#### Why I did it
`commands` module is not protected against malicious input
`getstatusoutput` is detected without a static string, uses `shell=True`
#### How I did it
Eliminate the use of `commands`
Use `subprocess.run()`, commands in `subprorcess.run()` are totally static
Fix indentation
#### How to verify it
Tested on DUT
[dell_log.txt](https://github.com/sonic-net/sonic-buildimage/files/9561332/dell_log.txt)
@xumia
Copy link
Collaborator

xumia commented Jul 4, 2023

@maipbui , we need to add the request tag for old release, right?

@qiluo-msft
Copy link
Collaborator

@xumia No need to backport. Please ping me if you see any blocking issues.

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.

3 participants