diff --git a/doc/MANUAL.md b/doc/MANUAL.md index b65a004..49a29c5 100644 --- a/doc/MANUAL.md +++ b/doc/MANUAL.md @@ -42,6 +42,7 @@ This is the manual for version %%VERSION%%. 1. [Script execution](#script_execution) - [Validation](#script_validation) - [Field Values](#script_fieldvalues) + - [Execution security policy](#script_runas) 1. [Users](#users) - [Passwords](#users_passwords) - [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 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. **Required**, **List of dictionaries**. @@ -871,7 +877,7 @@ out themselves. -## Script execution +## Script execution 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. @@ -944,6 +950,18 @@ ends. Examples of file uploads can be found in the `examples/simple` and `examples/megacorp` directories. +### Execution security policy + +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. + diff --git a/examples/run_as/README.md b/examples/run_as/README.md new file mode 100644 index 0000000..3c93b17 --- /dev/null +++ b/examples/run_as/README.md @@ -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! diff --git a/examples/run_as/job_run_as.py b/examples/run_as/job_run_as.py new file mode 100755 index 0000000..0462457 --- /dev/null +++ b/examples/run_as/job_run_as.py @@ -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)) + diff --git a/examples/run_as/run_as.json b/examples/run_as/run_as.json new file mode 100644 index 0000000..048fde1 --- /dev/null +++ b/examples/run_as/run_as.json @@ -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": [ + ] + } + ] +} diff --git a/src/formconfig.py b/src/formconfig.py index 57894a3..56d2e35 100644 --- a/src/formconfig.py +++ b/src/formconfig.py @@ -19,6 +19,7 @@ def run_as(uid, gid, groups): os.setuid(uid) return set_acc + class FormConfigError(Exception): """ Default error for FormConfig errors @@ -100,17 +101,26 @@ class FormConfig(object): for key, value in form_values.items(): env[key] = str(value) - # Get the user uid, gid and groups we should run as - pw = pwd.getpwnam(form.run_as) - gr = grp.getgrgid(pw.pw_gid) - 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}" - self.log.info(msg.format(pw.pw_name, gr.gr_name, str(groups))) - if os.getuid() != 0: - self.log.error("Not running as root! Running as different user " - "will probably fail!") + # 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) + else: + # Run as nobody + pw = pwd.getpwnam('nobody') + gr = grp.getgrgid(pw.pw_gid) + groups = [g.gr_gid for g in grp.getgrall() if pw.pw_name in g.gr_mem] + 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))) + else: + run_as_fn = None + 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 # the browser. Otherwise we store it for later displaying. @@ -121,7 +131,7 @@ class FormConfig(object): stderr=stderr, env=env, close_fds=True, - preexec_fn = run_as(uid, gid, groups)) + preexec_fn=run_as_fn) stdout, stderr = proc.communicate(input) return proc.returncode except OSError as err: @@ -136,7 +146,7 @@ class FormConfig(object): stderr=subprocess.PIPE, env=env, close_fds=True, - preexec_fn = run_as(uid, gid, groups)) + preexec_fn=run_as_fn) stdout, stderr = proc.communicate() return { 'stdout': stdout, diff --git a/src/formdefinition.py b/src/formdefinition.py index 38a8175..35be8de 100644 --- a/src/formdefinition.py +++ b/src/formdefinition.py @@ -19,7 +19,7 @@ class FormDefinition(object): """ def __init__(self, name, title, description, fields, script, output='escaped', hidden=False, submit_title="Submit", - allowed_users=None, run_as='nobody'): + allowed_users=None, run_as=None): self.name = name self.title = title self.description = description