If Scriptform is running as root, drop privileges to 'nobody' by default. Otherwise, don't drop privileges before executing scripts.

pull/7/head
Ferry Boender 9 years ago
parent 901e0d5938
commit 03f655b6e6
  1. 20
      doc/MANUAL.md
  2. 25
      examples/run_as/README.md
  3. 21
      examples/run_as/job_run_as.py
  4. 15
      examples/run_as/run_as.json
  5. 26
      src/formconfig.py
  6. 2
      src/formdefinition.py

@ -42,6 +42,7 @@ This is the manual for version %%VERSION%%.
1. [Script execution](#script_execution) 1. [Script execution](#script_execution)
- [Validation](#script_validation) - [Validation](#script_validation)
- [Field Values](#script_fieldvalues) - [Field Values](#script_fieldvalues)
- [Execution security policy](#script_runas)
1. [Users](#users) 1. [Users](#users)
- [Passwords](#users_passwords) - [Passwords](#users_passwords)
- [Form limiting](#users_formlimit) - [Form limiting](#users_formlimit)
@ -448,6 +449,11 @@ Structurally, they are made up of the following elements:
view it, if you know its name. This is useful for other forms to view it, if you know its name. This is useful for other forms to
redirect to this forms and such. redirect to this forms and such.
- **`run_as`**: Change to this user (and its groups) before running the
script. Only works if Scriptform is running as `root`. See also
[Execution security policy](#script_runas) **Optional**, **String**,
**Default:** `nobody`.
- **`fields`**: List of fields in the form. Each field is a dictionary. - **`fields`**: List of fields in the form. Each field is a dictionary.
**Required**, **List of dictionaries**. **Required**, **List of dictionaries**.
@ -871,7 +877,7 @@ out themselves.
## <a name="script_executing">Script execution</a> ## <a name="script_execution">Script execution</a>
When the user submits the form, Scriptform will validate the provided values. When the user submits the form, Scriptform will validate the provided values.
If they check out, the specified script for the form will be executed. If they check out, the specified script for the form will be executed.
@ -944,6 +950,18 @@ ends.
Examples of file uploads can be found in the `examples/simple` and Examples of file uploads can be found in the `examples/simple` and
`examples/megacorp` directories. `examples/megacorp` directories.
### <a name="script_runas">Execution security policy</a>
Running arbitrary scripts from Scriptform poses somewhat of a security risk.
Scriptform tries to mitigate this risk by running scripts as a different user
in some cases:
* If Scriptform itelf is running as root:
- By default, scripts will be run as user 'nobody'.
- If a form specifies as `run_as` field, scripts will be executed as that user.
* If Scriptform itself is running as a non-root user, scripts will be executed
as that user.

@ -0,0 +1,25 @@
ScriptForm test example
=========================
This test example shows the usage of the `run_as` functionality. If we specify a `run_as` field in a form like so:
"forms": [
{
"name": "run_as",
"title": "Run as...",
"description": "",
"submit_title": "Run",
"run_as": "man",
"script": "job_run_as.py",
"fields": []
}
]
Scriptform will try to run the script as that user (in this case: `man`). This
requires Scriptform to be running as root.
If no `run_as` is given in a script, Scriptform will execute scripts as the
current user (the one running Scriptform). If, however, Scriptform is being run
as root and you don't specify a `run_as` user, the scripts will run as user
`nobody` for security considerations!

@ -0,0 +1,21 @@
#!/usr/bin/python
import os
import pwd
import grp
pw = pwd.getpwuid(os.getuid())
gr = grp.getgrgid(pw.pw_gid)
groups = [g.gr_gid for g in grp.getgrall() if pw.pw_name in g.gr_mem]
priv_esc = True
try:
os.seteuid(0)
except OSError:
priv_esc = False
print """Running as:
uid = {0}
gid = {1}
groups = {2}""".format(pw.pw_uid, gr.gr_gid, str(groups))

@ -0,0 +1,15 @@
{
"title": "Run as",
"forms": [
{
"name": "run_as",
"title": "Run as...",
"description": "",
"submit_title": "Run",
"run_as": "man",
"script": "/tmp/test/job_run_as.py",
"fields": [
]
}
]
}

@ -19,6 +19,7 @@ def run_as(uid, gid, groups):
os.setuid(uid) os.setuid(uid)
return set_acc return set_acc
class FormConfigError(Exception): class FormConfigError(Exception):
""" """
Default error for FormConfig errors Default error for FormConfig errors
@ -100,17 +101,26 @@ class FormConfig(object):
for key, value in form_values.items(): for key, value in form_values.items():
env[key] = str(value) env[key] = str(value)
# Get the user uid, gid and groups we should run as # Get the user uid, gid and groups we should run as. If the current
# user is root, we run as the given user or 'nobody' if no user was
# specified. Otherwise, we run as the user we already are.
run_as_uid = None
if os.getuid() == 0:
if form.run_as is not None:
pw = pwd.getpwnam(form.run_as) pw = pwd.getpwnam(form.run_as)
else:
# Run as nobody
pw = pwd.getpwnam('nobody')
gr = grp.getgrgid(pw.pw_gid) gr = grp.getgrgid(pw.pw_gid)
groups = [g.gr_gid for g in grp.getgrall() if pw.pw_name in g.gr_mem] groups = [g.gr_gid for g in grp.getgrall() if pw.pw_name in g.gr_mem]
uid = pw.pw_uid
gid = pw.pw_gid
msg = "Running script as user={0}, gid={1}, groups={2}" msg = "Running script as user={0}, gid={1}, groups={2}"
run_as_fn = run_as(pw.pw_uid, pw.pw_gid, groups)
self.log.info(msg.format(pw.pw_name, gr.gr_name, str(groups))) self.log.info(msg.format(pw.pw_name, gr.gr_name, str(groups)))
if os.getuid() != 0: else:
self.log.error("Not running as root! Running as different user " run_as_fn = None
"will probably fail!") if form.run_as is not None:
self.log.critical("Not running as root, so we can't run the script"
"as user '{0}'".format(form.run_as))
# If the form output type is 'raw', we directly stream the output to # If the form output type is 'raw', we directly stream the output to
# the browser. Otherwise we store it for later displaying. # the browser. Otherwise we store it for later displaying.
@ -121,7 +131,7 @@ class FormConfig(object):
stderr=stderr, stderr=stderr,
env=env, env=env,
close_fds=True, close_fds=True,
preexec_fn = run_as(uid, gid, groups)) preexec_fn=run_as_fn)
stdout, stderr = proc.communicate(input) stdout, stderr = proc.communicate(input)
return proc.returncode return proc.returncode
except OSError as err: except OSError as err:
@ -136,7 +146,7 @@ class FormConfig(object):
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
env=env, env=env,
close_fds=True, close_fds=True,
preexec_fn = run_as(uid, gid, groups)) preexec_fn=run_as_fn)
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
return { return {
'stdout': stdout, 'stdout': stdout,

@ -19,7 +19,7 @@ class FormDefinition(object):
""" """
def __init__(self, name, title, description, fields, script, def __init__(self, name, title, description, fields, script,
output='escaped', hidden=False, submit_title="Submit", output='escaped', hidden=False, submit_title="Submit",
allowed_users=None, run_as='nobody'): allowed_users=None, run_as=None):
self.name = name self.name = name
self.title = title self.title = title
self.description = description self.description = description

Loading…
Cancel
Save