Search code examples
pythonlinuxsecurityprocesselevated-privileges

Secure child to parent communication in python


My python program needs elevated privileges and is therefore started by root (with a setuid-binary-wrapper).

To keep the attack surface (and the impact of coding errors) as minimal as possible, I decided to split my code into two parts: One part is to be executed as root and the other with regular user permissions. The problem is, that the code is interdependent and therefore needs secure two-way communication.

I do not know if this is the right approach (other ideas welcome), but I decided to go with two processes - one parent process with elevated privileges and one child process with the privileges of a regular user.

The idea:

  • The parent process is started by root and keeps its privileges
  • The parent process spawns a child process which drops to a regular user (the child process cannot regain root privileges)
  • The child process does most of the work, but if it needs to execute something with root rights, it tells the parent process to do this for it

The questions:

  • Is subprocess(.Popen) sufficient for this? Would multiprocessing be better suited?

  • How can the child process and the parent communicate in an interactive and secure way (Is subprocess.PIPE secure)?

  • Do you know of any simple code examples somewhere for this scenario?

-------------------------------------------------------------------------

Based on Gil Hamilton's suggestions, I came up with below code

Some questions remain:

  • Is this secure? Do I need to drop additional stuff like file descriptors or is os.setuid(<unprivileged UID>) sufficient?
  • In general, if a process drops to a specific user like this, is this user able to interfere with the dropped processes memory?

privileged.py:

#!/bin/python
from multiprocessing import Process, Pipe
from unprivileged import Unprivileged

if __name__ == '__main__':
  privilegedProcessPipeEnd, unprivilegedProcessPipeEnd = Pipe()
  unprivilegedProcess = Process(target=Unprivileged(unprivilegedProcessPipeEnd).operate)
  unprivilegedProcess.start()

  print(privilegedProcessPipeEnd.recv())
  privilegedProcessPipeEnd.send("ok")
  print(privilegedProcessPipeEnd.recv())
  privilegedProcessPipeEnd.send("nok")

  privilegedProcessPipeEnd.close()
  unprivilegedProcessPipeEnd.close()
  unprivilegedProcess.join()

unprivileged.py:

import os

class Unprivileged:

  def __init__(self, unprivilegedProcessPipeEnd):
    self._unprivilegedProcessPipeEnd = unprivilegedProcessPipeEnd

  def operate(self):
    invokerUid = os.getuid()

    if invokerUid == 0:
      # started by root; TODO: drop to predefined standard user
      # os.setuid(standardUid)
      pass
    else:
      # started by a regular user through a setuid-binary 
      os.setuid(invokerUid) # TODO: drop to predefined standard user (save invokerUid for future stuff)

    # os.setuid(0) # not permitted anymore, cannot become root again

    print("os.getuid(): " + str(os.getuid()))

    self._unprivilegedProcessPipeEnd.send("invoke privilegedFunction1")
    print(self._unprivilegedProcessPipeEnd.recv())
    self._unprivilegedProcessPipeEnd.send("invoke privilegedFunction2")
    print(self._unprivilegedProcessPipeEnd.recv())

    return

main.c (setuid-wrapper program):

#include <unistd.h>
#define SCRIPT_PATH "/home/u1/project/src/privileged.py"

int
main(int argc,
     char **argv) {
  return execv(SCRIPT_PATH, argv);
}

/* compile and run like this:
$ gcc -std=c99 main.c -o main
# chown root:root main
# chmod 6771 main
$ chmod +x /home/u1/project/src/privileged.py
$ ./main
*/

Solution

  • This can be done with Popen but it's a bit clumsy IMO because you have no control over the process transition with Popen. If you're relying on the UID to attenuate privileges, you need to fork and then in the child, adjust your UID, before invoking other child code.

    (No real reason you couldn't put your child code in a separate program that you invoke with Popen and have it adjust its UID as the first step, it just seems to me a strange way to structure it.)

    I would recommend that you look at using the multiprocessing module. That module makes it easy to create a new process (it will handle the fork for you). Then you can easily drop in code that adjusts the UID (see below) and then you can just run the child code within the same "code base". That is, you don't necessarily need to invoke a separate program.

    The multiprocessing module also provides its own Pipe object as well as a Queue object, both of which are inter-process communication mechanisms. Both are secure -- in the sense that no external user can horn in on them (without already having root privilege). But of course if your unprivileged child process is compromised, it can send whatever it wants to the parent so your privileged parent will still need to validate / audit its input.

    The documentation for the multiprocessing module provides several simple examples that should get you started. Once created, using a pipe is as easy as reading and writing a file.

    As for adjusting the UID, that's simply a single call to os.setuid in the child before invoking the code you want to run as an unprivileged user. Read the setuid(2) and credentials(7) manual pages for more information.