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