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

New python rule to check for setuid(0) #589

Merged
merged 1 commit into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@
| PY035 | [hashlib — improper prng](rules/python/stdlib/hashlib-improper-prng.md) | Improper Randomness for Cryptographic `hashlib` Functions |
| PY036 | [os — incorrect permission](rules/python/stdlib/os-loose-file-perm.md) | Incorrect Permission Assignment for Critical Resource using `os` Module |
| PY037 | [pathlib — incorrect permission](rules/python/stdlib/pathlib-loose-file-perm.md) | Incorrect Permission Assignment for Critical Resource using `pathlib` Module |
| PY038 | [os — unnecessary privileges](rules/python/stdlib/os-setuid-root.md) | Execution with Unnecessary Privileges using `os` Module |
10 changes: 10 additions & 0 deletions docs/rules/python/stdlib/os-setuid-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
id: PY038
title: os — unnecessary privileges
hide_title: true
pagination_prev: null
pagination_next: null
slug: /rules/PY038
---

::: precli.rules.python.stdlib.os_setuid_root
1 change: 1 addition & 0 deletions precli/core/cwe.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Cwe:
208: "Observable Timing Discrepancy",
214: "Invocation of Process Using Visible Sensitive Information",
215: "Insertion of Sensitive Information Into Debugging Code",
250: "Execution with Unnecessary Privileges",
295: "Improper Certificate Validation",
319: "Cleartext Transmission of Sensitive Information",
326: "Inadequate Encryption Strength",
Expand Down
98 changes: 98 additions & 0 deletions precli/rules/python/stdlib/os_setuid_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Copyright 2024 Secure Sauce LLC
r"""
# Execution with Unnecessary Privileges using `os` Module

The Python function os.setuid() is used to set the user ID of the current
process. Passing a user ID of 0 to setuid() changes the process’s user to the
root user (superuser). This can lead to privilege escalation, allowing the
current process to execute with root-level permissions, which could be
exploited by malicious actors to gain control over the system.

Processes running with elevated privileges (such as root) can pose significant
security risks if misused. For instance, a vulnerability in such a process
could be leveraged by attackers to compromise the entire system. Therefore,
it is essential to avoid changing the process’s user ID to 0 unless absolutely
necessary and to ensure such usage is thoroughly reviewed and justified.

## Examples

```python linenums="1" hl_lines="4" title="os_setuid_0.py"
import os


os.setuid(0)
```

??? example "Example Output"
```
> precli tests/unit/rules/python/stdlib/os/examples/os_setuid_0.py
⚠️ Warning on line 9 in tests/unit/rules/python/stdlib/os/examples/os_setuid_0.py
PY038: Execution with Unnecessary Privileges
The function 'os.setuid(0)' escalates the process to run with root (superuser) privileges.
```

## Remediation

- Avoid using setuid(0) unless absolutely necessary: Review whether running
as the root user is required for the task at hand. It is safer to operate
with the least privileges necessary.
- Drop privileges as soon as possible: If elevated privileges are required
temporarily, ensure that the process drops those privileges immediately
after performing the necessary tasks.
- Validate input to avoid malicious manipulation: If input parameters control
the user ID passed to setuid(), ensure they are securely validated and not
influenced by untrusted sources.
- Use alternatives to running as root: If feasible, design your application
to avoid needing root privileges entirely. Consider utilizing a dedicated
service or capability that performs the task in a secure, controlled manner.

```python linenums="1" hl_lines="4" title="os_setuid_0.py"
import os


os.setuid(1000)
```

## See also

!!! info
- [os — Miscellaneous operating system interfaces — Python documentation](https://docs.python.org/3/library/os.html#os.setuid)
- [Principle of Least Privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege)
- [CWE-250: Execution with Unnecessary Privileges](https://cwe.mitre.org/data/definitions/250.html)

_New in version 0.6.6_

""" # noqa: E501
from precli.core.call import Call
from precli.core.config import Config
from precli.core.level import Level
from precli.core.location import Location
from precli.core.result import Result
from precli.rules import Rule


class OsSetuidRoot(Rule):
def __init__(self, id: str):
super().__init__(
id=id,
name="unnecessary_privileges",
description=__doc__,
cwe_id=250,
message="The function '{0}(0)' escalates the process to run with "
"root (superuser) privileges.",
config=Config(level=Level.ERROR),
)

def analyze_call(self, context: dict, call: Call) -> Result | None:
if call.name_qualified != "os.setuid":
return

argument = call.get_argument(position=0, name="uid")
uid = argument.value

if isinstance(uid, int) and uid == 0:
return Result(
rule_id=self.id,
location=Location(node=argument.node),
message=self.message.format(call.name_qualified),
)
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,6 @@ precli.rules.python =

# precli/rules/python/stdlib/pathlib_loose_file_perm.py
PY037 = precli.rules.python.stdlib.pathlib_loose_file_perm:PathlibLooseFilePermissions

# precli/rules/python/stdlib/os_setuid_root.py
PY038 = precli.rules.python.stdlib.os_setuid_root:OsSetuidRoot
9 changes: 9 additions & 0 deletions tests/unit/rules/python/stdlib/os/examples/os_setuid_0.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# level: ERROR
# start_line: 9
# end_line: 9
# start_column: 10
# end_column: 11
import os


os.setuid(0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# level: NONE
import os


os.setuid(1000)
10 changes: 10 additions & 0 deletions tests/unit/rules/python/stdlib/os/examples/os_setuid_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# level: ERROR
# start_line: 10
# end_line: 10
# start_column: 10
# end_column: 14
import os


root = 0
os.setuid(root)
49 changes: 49 additions & 0 deletions tests/unit/rules/python/stdlib/os/test_os_setuid_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright 2024 Secure Sauce LLC
import os

import pytest

from precli.core.level import Level
from precli.parsers import python
from precli.rules import Rule
from tests.unit.rules import test_case


class TestOsSetuidRoot(test_case.TestCase):
@classmethod
def setup_class(cls):
cls.rule_id = "PY038"
cls.parser = python.Python()
cls.base_path = os.path.join(
"tests",
"unit",
"rules",
"python",
"stdlib",
"os",
"examples",
)

def test_rule_meta(self):
rule = Rule.get_by_id(self.rule_id)
assert rule.id == self.rule_id
assert rule.name == "unnecessary_privileges"
assert (
rule.help_url
== f"https://docs.securesauce.dev/rules/{self.rule_id}"
)
assert rule.default_config.enabled is True
assert rule.default_config.level == Level.ERROR
assert rule.default_config.rank == -1.0
assert rule.cwe.id == 250

@pytest.mark.parametrize(
"filename",
[
"os_setuid_0.py",
"os_setuid_1000.py",
"os_setuid_root.py",
],
)
def test(self, filename):
self.check(filename)