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

Feature: allow command with specific parameters (Closes: #168) #233

Merged
merged 2 commits into from
Oct 26, 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
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,24 @@ You might need to ensure that lshell is listed in /etc/shells.

### lshell.conf

lshell.conf presents a template configuration file. See etc/lshell.conf or man file for more information.
#### Allowed list
lshell.conf presents a template configuration file. See `etc/lshell.conf` or the man file for more information.

You can allow commands specifying commands with exact arguments in the `allowed` list. This means you can define specific commands along with their arguments that are permitted. Commands without arguments can also be specified, allowing any arguments to be passed.

For example:
```
allowed: ['ls', 'echo asd', 'telnet localhost']
```

This will:
- Allow the `ls` command with any arguments.
- Allow `echo asd` but will reject `echo` with any other arguments (e.g., `echo qwe` will be rejected).
- Allow `telnet localhost`, but not `telnet` with other hosts (e.g., `telnet 192.168.0.1` will be rejected).

Commands that do not include arguments (e.g., `ls`) can be used with any arguments, while commands specified with arguments (e.g., `echo asd`) must be used exactly as specified.

#### User profiles

A [default] profile is available for all users using lshell. Nevertheless, you can create a [username] section or a [grp:groupname] section to customize users' preferences.

Expand All @@ -92,7 +109,9 @@ Order of priority when loading preferences is the following:
3. Default configuration

The primary goal of lshell, is to be able to create shell accounts with ssh access and restrict their environment to a couple a needed commands and path.


#### Example

For example User 'foo' and user 'bar' both belong to the 'users' UNIX group:

- User 'foo':
Expand Down
1 change: 1 addition & 0 deletions etc/lshell.conf
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ loglevel : 2
## thus easily escaping lshell. See variable 'path_noexec' to use an alternative
## path to library.
allowed : ['ls', 'echo','ll']
#allowed : ['echo test'] # this will allow only the command 'echo test'

## A list of the allowed commands that are permitted to execute other
## programs (e.g. shell scripts with exec(3)). Setting this variable to 'all'
Expand Down
45 changes: 27 additions & 18 deletions lshell/sec.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,32 +229,41 @@ def check_secure(line, conf, strict=None, ssh=None):
separate_line = re.sub(r"\)$", "", separate_line)
separate_line = " ".join(separate_line.split())
splitcmd = separate_line.strip().split(" ")

# Extract the command and its arguments
command = splitcmd[0]
if len(splitcmd) > 1:
cmdargs = splitcmd
else:
cmdargs = None
command_args_list = splitcmd[1:]
command_args_string = " ".join(command_args_list)
full_command = f"{command} {command_args_string}".strip()

# in case of a sudo command, check in sudo_commands list if allowed
if command == "sudo":
if isinstance(cmdargs, list):
# allow the -u (user) flag
if cmdargs[1] == "-u" and cmdargs:
sudocmd = cmdargs[3]
else:
sudocmd = cmdargs[1]
if sudocmd not in conf["sudo_commands"] and cmdargs:
ret, conf = warn_count(
"sudo command", oline, conf, strict=strict, ssh=ssh
)
return ret, conf
if command == "sudo" and command_args_list:
# allow the -u (user) flag
if command_args_list[0] == "-u" and command_args_list:
sudocmd = command_args_list[2]
else:
sudocmd = command_args_list[0]
if sudocmd not in conf["sudo_commands"] and command_args_list:
ret, conf = warn_count(
"sudo command", oline, conf, strict=strict, ssh=ssh
)
return ret, conf

# if over SSH, replaced allowed list with the one of overssh
if ssh:
conf["allowed"] = conf["overssh"]

# for all other commands check in allowed list
if command not in conf["allowed"] and command:
# # for all other commands check in allowed list
# if command not in conf["allowed"] and command:
# ret, conf = warn_count("command", command, conf, strict=strict, ssh=ssh)
# return ret, conf

# Check if the full command (with arguments) or just the command is allowed
if (
full_command not in conf["allowed"]
and command not in conf["allowed"]
and command
):
ret, conf = warn_count("command", command, conf, strict=strict, ssh=ssh)
return ret, conf

Expand Down
2 changes: 1 addition & 1 deletion lshell/shellcmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def __getattr__(self, attr):
):
utils.exec_cmd(f'echo "WinSCP: this is end-of-file: {self.retcode}"')
return object.__getattribute__(self, attr)
if self.g_cmd in self.conf["allowed"]:
if self.g_cmd in self.conf["allowed"] or self.g_line in self.conf["allowed"]:
if self.conf["timer"] > 0:
self.mytimer(0)
self.g_arg = re.sub("^~$|^~/", f"{self.conf['home_path']}/", self.g_arg)
Expand Down
2 changes: 1 addition & 1 deletion lshell/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys
import os

__version__ = "0.10.2"
__version__ = "0.10.3-rc1"

# Required config variable list per user
required_config = ["allowed", "forbidden", "warning_counter"]
Expand Down
18 changes: 14 additions & 4 deletions man/lshell.1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.\"
.\" Man page for the Limited Shell (lshell) project.
.\"
.TH lshell 1 "October, 2024" "v0.10.2"
.TH lshell 1 "October, 2024" "v0.10.3-rc1"

.SH NAME
lshell \- Limited Shell
Expand Down Expand Up @@ -150,9 +150,19 @@ using the -exec flag.
command aliases list (similar to bash's alias directive)
.TP
.I allowed
a list of the allowed commands or set to 'all' to allow all commands in user's \
PATH

A list of allowed commands, or set to 'all' to allow all commands in the user's PATH.
.RS
.BR \ \ \ \ -
If a command is specified without arguments (e.g., 'echo'), the command is allowed to run with any arguments.
.RE
.RS
.BR \ \ \ \ -
If a command is specified with exact arguments (e.g., 'echo asd'), only that specific command with those arguments will be allowed.
.RE
.RS
.BR \ \ \ \ -
To allow all commands in the user's PATH, use the value 'all'.
.RE
if sudo(8) is installed and sudo_noexec.so is available, it will be loaded
before running every command, preventing it from running further commands
itself. If not available, beware of commands like vim/find/more/etc. that will
Expand Down
83 changes: 74 additions & 9 deletions test/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ def test_31_security_echo_freedom_and_help(self):
# Verify the combined output
self.assertEqual(expected_output, result)

def test_31_security_echo_freedom_and_cd(self):
def test_32_security_echo_freedom_and_cd(self):
"""F32 | test echo FREEDOM! && cd () bash && cd ~/"""
self.child = pexpect.spawn(
f"{TOPDIR}/bin/lshell " f"--config {TOPDIR}/etc/lshell.conf "
Expand All @@ -534,7 +534,7 @@ def test_31_security_echo_freedom_and_cd(self):
# Verify the combined output
self.assertEqual(expected_output, result)

def test_32_ls_non_existing_directory_and_echo(self):
def test_33_ls_non_existing_directory_and_echo(self):
"""Test: ls non_existing_directory && echo nothing"""
self.child = pexpect.spawn(
f"{TOPDIR}/bin/lshell --config {TOPDIR}/etc/lshell.conf"
Expand All @@ -548,7 +548,7 @@ def test_32_ls_non_existing_directory_and_echo(self):
# Since ls fails, echo nothing shouldn't run
self.assertNotIn("nothing", output)

def test_33_ls_and_echo_ok(self):
def test_34_ls_and_echo_ok(self):
"""Test: ls && echo OK"""
self.child = pexpect.spawn(
f"{TOPDIR}/bin/lshell --config {TOPDIR}/etc/lshell.conf"
Expand All @@ -562,7 +562,7 @@ def test_33_ls_and_echo_ok(self):
# ls succeeds, echo OK should run
self.assertIn("OK", output)

def test_34_ls_non_existing_directory_or_echo_ok(self):
def test_35_ls_non_existing_directory_or_echo_ok(self):
"""Test: ls non_existing_directory || echo OK"""
self.child = pexpect.spawn(
f"{TOPDIR}/bin/lshell --config {TOPDIR}/etc/lshell.conf"
Expand All @@ -576,7 +576,7 @@ def test_34_ls_non_existing_directory_or_echo_ok(self):
# ls fails, echo OK should run
self.assertIn("OK", output)

def test_35_ls_or_echo_nothing(self):
def test_36_ls_or_echo_nothing(self):
"""Test: ls || echo nothing"""
self.child = pexpect.spawn(
f"{TOPDIR}/bin/lshell --config {TOPDIR}/etc/lshell.conf"
Expand All @@ -590,7 +590,7 @@ def test_35_ls_or_echo_nothing(self):
# ls succeeds, echo nothing should not run
self.assertNotIn("nothing", output)

def test_36_env_vars_file_not_found(self):
def test_37_env_vars_file_not_found(self):
"""Test missing environment variable file"""
missing_file_path = "/path/to/missing/file"

Expand All @@ -613,7 +613,7 @@ def test_36_env_vars_file_not_found(self):
# Check the error message in the output
self.assertIn(expected, self.child.before.decode("utf8"))

def test_37_load_env_vars_from_file(self):
def test_38_load_env_vars_from_file(self):
"""Test loading environment variables from file"""

# Create a temporary file to store environment variables
Expand Down Expand Up @@ -641,7 +641,7 @@ def test_37_load_env_vars_from_file(self):
# Cleanup the temporary file
os.remove(temp_env_file_path)

def test_38_script_execution_with_template(self):
def test_39_script_execution_with_template(self):
"""Test executing script after modifying shebang and clean up afterward"""

template_path = f"{TOPDIR}/test/template.lsh"
Expand Down Expand Up @@ -693,7 +693,7 @@ def test_38_script_execution_with_template(self):
if os.path.exists(test_script_path):
os.remove(test_script_path)

def test_39_script_execution_with_template_strict(self):
def test_40_script_execution_with_template_strict(self):
"""Test executing script after modifying shebang and clean up afterward"""

template_path = f"{TOPDIR}/test/template.lsh"
Expand Down Expand Up @@ -750,3 +750,68 @@ def test_39_script_execution_with_template_strict(self):
os.remove(test_script_path)
if os.path.exists(wrapper_path):
os.remove(wrapper_path)

def test_41_multicmd_with_wrong_arg_should_fail(self):
"""F20 | Allowing 'echo asd': Test 'echo qwe' should fail"""
self.child = pexpect.spawn(
f"{TOPDIR}/bin/lshell "
f"--config {TOPDIR}/etc/lshell.conf "
"--allowed \"['echo asd']\""
)
self.child.expect(f"{self.user}:~\\$")

expected = "*** forbidden command: echo"

self.child.sendline("echo qwe")
self.child.expect(f"{self.user}:~\\$")
result = self.child.before.decode("utf8").split("\n")[1].strip()
self.assertEqual(expected, result)

def test_42_multicmd_with_near_exact_arg_should_fail(self):
"""F41 | Allowing 'echo asd': Test 'echo asds' should fail"""
self.child = pexpect.spawn(
f"{TOPDIR}/bin/lshell "
f"--config {TOPDIR}/etc/lshell.conf "
"--allowed \"['echo asd']\""
)
self.child.expect(f"{self.user}:~\\$")

expected = "*** forbidden command: echo"

self.child.sendline("echo asds")
self.child.expect(f"{self.user}:~\\$")
result = self.child.before.decode("utf8").split("\n")[1].strip()
self.assertEqual(expected, result)

def test_43_multicmd_without_arg_should_fail(self):
"""F42 | Allowing 'echo asd': Test 'echo' should fail"""
self.child = pexpect.spawn(
f"{TOPDIR}/bin/lshell "
f"--config {TOPDIR}/etc/lshell.conf "
"--allowed \"['echo asd']\""
)
self.child.expect(f"{self.user}:~\\$")

expected = "*** forbidden command: echo"

self.child.sendline("echo")
self.child.expect(f"{self.user}:~\\$")
result = self.child.before.decode("utf8").split("\n")[1].strip()
self.assertEqual(expected, result)

def test_44_multicmd_asd_should_pass(self):
"""F43 | Allowing 'echo asd': Test 'echo asd' should pass"""

self.child = pexpect.spawn(
f"{TOPDIR}/bin/lshell "
f"--config {TOPDIR}/etc/lshell.conf "
"--allowed \"['echo asd']\""
)
self.child.expect(f"{self.user}:~\\$")

expected = "asd"

self.child.sendline("echo asd")
self.child.expect(f"{self.user}:~\\$")
result = self.child.before.decode("utf8").split("\n")[1].strip()
self.assertEqual(expected, result)
Loading