Repository: benoitc/gunicorn

Issue:
Incomplete privileges drop
Hello,

Seems uid/gid drop is incomplete on gunicorn. Setting uid doesn’t fetch real uid groups.
This behaviour is visible here :

```
# cat foo.py
#!/usr/bin/env python
import os
def application(env, response):
    response('200 OK', [('Content-Type', 'text/plain')])
    return [str((os.getuid(), os.getgid(), os.getgroups()))]

def nothing(status, headers):
    pass

if __name__ == '__main__':
    print(application(None, nothing))

# groups
root
# ./foo.py 
['(0, 0, [0])']  // Groups OK, [root]

# gunicorn -b 127.0.0.1:5000 foo:application
# curl localhost:5000
(0, 0, [0])  // Groups OK, [root]

# gunicorn -u www-data -g www-data -b 127.0.0.1:5000 foo:application
# curl localhost:5000
(33, 33, []) // Groups not OK, empty…

# su www-data -s /bin/sh
$ groups
www-data debian-tor
$ ./foo.py
['(33, 33, [33, 117])'] // Groups OK, [www-data, debian-tor]

$ gunicorn -b 127.0.0.1:5000 foo:application
$ curl localhost:5000
(33, 33, [33, 117])  // Groups OK, [www-data, debian-tor]
```

Running python script directly, groups are set for root (root) and www-data (www-data and debian-tor).
Running wsgi with gunicorn, ok from root and www-data.
Running wsgi with gunicorn from root and uid/gid www-data, not ok, groups are empty.

This is a problem because after privileges dropping, your app can’t access file with group restriction (in my case, unable to access file with debian-tor:debian-tor chown and 770 chmod).

Seems this is because groups are not initializated (`os.initgroups(username, groups)`) before privileges drop.
[This patch](https://github.com/aeris/gunicorn/commit/696c4bfd57ff851018687f7011a8e57608fcc367) may fix the trouble, but is not portable (UNIX only because of `pwd` package usage).



Discussion:

Resolving patch:
diff --git a/gunicorn/config.py b/gunicorn/config.py
--- a/gunicorn/config.py
+++ b/gunicorn/config.py
@@ -987,6 +987,23 @@ class Umask(Setting):
         """
 
 
+class Initgroups(Setting):
+    name = "initgroups"
+    section = "Server Mechanics"
+    cli = ["--initgroups"]
+    validator = validate_bool
+    action = 'store_true'
+    default = False
+
+    desc = """\
+        If true, set the worker process's group access list with all of the
+        groups of which the specified username is a member, plus the specified
+        group id.
+
+        .. versionadded:: 19.7
+        """
+
+
 class TmpUploadDir(Setting):
     name = "tmp_upload_dir"
     section = "Server Mechanics"
diff --git a/gunicorn/util.py b/gunicorn/util.py
--- a/gunicorn/util.py
+++ b/gunicorn/util.py
@@ -54,6 +54,7 @@
 
 try:
     from setproctitle import setproctitle
+
     def _setproctitle(title):
         setproctitle("gunicorn: %s" % title)
 except ImportError:
@@ -147,13 +148,30 @@ def load_class(uri, default="gunicorn.workers.sync.SyncWorker",
         return getattr(mod, klass)
 
 
-def set_owner_process(uid, gid):
+def get_username(uid):
+    """ get the username for a user id"""
+    return pwd.getpwuid(uid).pw_name
+
+
+def set_owner_process(uid, gid, initgroups=False):
     """ set user and group of workers processes """
+
     if gid:
+        if uid:
+            try:
+                username = get_username(uid)
+            except KeyError:
+                initgroups = False
+
         # versions of python < 2.6.2 don't manage unsigned int for
         # groups like on osx or fedora
         gid = abs(gid) & 0x7FFFFFFF
-        os.setgid(gid)
+
+        if initgroups:
+            os.initgroups(username, gid)
+        else:
+            os.setgid(gid)
+
     if uid:
         os.setuid(uid)
 
diff --git a/gunicorn/workers/base.py b/gunicorn/workers/base.py
--- a/gunicorn/workers/base.py
+++ b/gunicorn/workers/base.py
@@ -102,7 +102,8 @@ def changed(fname):
             for k, v in self.cfg.env.items():
                 os.environ[k] = v
 
-        util.set_owner_process(self.cfg.uid, self.cfg.gid)
+        util.set_owner_process(self.cfg.uid, self.cfg.gid,
+                               initgroups=self.cfg.initgroups)
 
         # Reseed the random number generator
         util.seed()


Test patch: