{"id":25566,"date":"2020-05-15T12:31:48","date_gmt":"2020-05-15T10:31:48","guid":{"rendered":"https:\/\/pi3g.com\/?p=25566"},"modified":"2020-05-15T12:40:10","modified_gmt":"2020-05-15T10:40:10","slug":"secure-command-execution-with-python-subprocess-popen","status":"publish","type":"post","link":"https:\/\/pi3g.com\/de\/secure-command-execution-with-python-subprocess-popen\/","title":{"rendered":"sichere Befehlsausf\u00fchrung mit Python: subprocess.Popen"},"content":{"rendered":"<p>Security is important for me while developing the picockpit-client. <\/p>\n<p>The following applies to Linux systems (but probably is applicable to all Unix like systems, including macOS)<\/p>\n<p>Python allows to run external commands using the subprocess module.<\/p>\n<blockquote>\n<p>import subprocess<\/p>\n<\/blockquote>\n<p>In the upcoming version of PiCockpit, users will be able to create their own buttons (simply editing a JSON file on the Pi) in order to run commands.<\/p>\n<p>PiCockpit will ship with three buttons by default:<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-10.png\"><img loading=\"lazy\" decoding=\"async\" width=\"669\" height=\"292\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-10.png\" border=\"0\"><\/a><\/p>\n<p>These commands all require root privileges. picockpit-client, per default, runs with root privileges on your system.<\/p>\n<p>But other commands you want to execute, should run as less privileged users (we default to the user \u201cpi\u201d). In fact, some commands, like the Chromium Browser will <strong>refuse <\/strong>to run as root.<\/p>\n<p>In short, we need a solution to run these processes using a different user.<\/p>\n<p>This is possible, thanks to the <strong>preexec_fn<\/strong> parameter for popen.<\/p>\n<p>Here is my code:<\/p>\n<blockquote>\n<p>def thread_run(self):<br \/>&nbsp;&nbsp;&nbsp;&nbsp; def sanitize(input_string):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; #<a href=\"https:\/\/maxharp3r.wordpress.com\/2008\/05\/15\/pythons-minidom-xml-and-illegal-unicode-characters\/\">https:\/\/maxharp3r.wordpress.com\/2008\/05\/15\/pythons-minidom-xml-and-illegal-unicode-characters\/<\/a><br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; RE_XML_ILLEGAL = u'([\\u0000-\\u0008\\u000b-\\u000c\\u000e-\\u001f\\ufffe-\\uffff])&#8217; + \\<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; u&#8217;|&#8217; + \\<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])&#8217; % \\<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff),<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff),<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff))<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return re.sub(RE_XML_ILLEGAL, &#8220;&#8221;, input_string)<\/p>\n<p>&nbsp;&nbsp;&nbsp; def demote(user, uid, gid):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; def demote_function():<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8220;starting&#8221;)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print (&#8216;uid, gid = %d, %d&#8217; % (os.getuid(), os.getgid()))<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print (os.getgroups())<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # initgroups must be run before we lose the privilege to set it!<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.initgroups(user, gid)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.setgid(gid)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # this must be run last<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.setuid(uid)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8220;finished demotion&#8221;)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8216;uid, gid = %d, %d&#8217; % (os.getuid(), os.getgid()))<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print (os.getgroups())<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return demote_function<\/p>\n<p>&nbsp;&nbsp;&nbsp; def run_cmd(cmd, cmd_def):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_code = 0<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_error = &#8220;&#8221;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_result = &#8220;&#8221;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; timeout = None<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; user = &#8220;pi&#8221;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if &#8220;timeout&#8221; in cmd_def:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if isinstance(cmd_def[&#8220;timeout&#8221;], int):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if cmd_def[&#8220;timeout&#8221;] &gt; 0:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; timeout = cmd_def[&#8220;timeout&#8221;]<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if &#8220;user&#8221; in cmd_def:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if isinstance(cmd_def[&#8220;user&#8221;], str):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; user = cmd_def[&#8220;user&#8221;]<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; try:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8220;picontrol :: setup of user rights&#8221;)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; uid = pwd.getpwnam(user).pw_uid<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; gid = pwd.getpwnam(user).pw_gid<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8220;picontrol :: requested user &#8221; + user + &#8221; uid &#8221; + str(uid) + &#8221; gid &#8221; + str(gid))<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8220;picontrol :: starting command &#8230; &#8220;)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(cmd)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; proc = subprocess.Popen(<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cmd,<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; stdout=subprocess.PIPE,<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; stderr=subprocess.PIPE,<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; preexec_fn=demote(user, uid, gid)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; )<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # TODO: handle timeout differently<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # timeout=timeout)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8220;picontrol :: AFTER subprocess.Popen&#8221;)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; except FileNotFoundError:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_code = errno.ENOENT<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_error = &#8220;Command not found on your system &#8221; + &#8221; &#8220;.join(cmd)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # TODO: will this actually ever be called?<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; except subprocess.TimeoutExpired:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_code = errno.ETIME<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_error = &#8220;Command timeout expired (&#8221; + str(cmd_def[&#8220;timeout&#8221;]) + &#8221; sec)&#8221;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; except Exception as e:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if hasattr(e, &#8216;errno&#8217;):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_code = e.errno<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_code = 1<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if hasattr(e, &#8216;message&#8217;):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_error = str(e.message)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_error = str(e)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # process can execute normally, no exceptions at startup<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; process_running = True<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; while process_running:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if proc.poll() is not None:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; process_running = False<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if self.stop_flag.is_set():<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8220;stop flag received in process_running, setting process_running to false&#8221;)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; process_running = False<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # and also do steps for active termination after that.<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # half a second of resolution<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.sleep(0.5)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_code = proc.returncode<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_error = sanitize(proc.stderr.read().decode(&#8216;UTF-8&#8217;).rstrip(&#8220;\\r\\n&#8221;))<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_result = sanitize(proc.stdout.read().decode(&#8216;UTF-8&#8217;).rstrip(&#8220;\\r\\n&#8221;))<\/p>\n<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return (return_code, return_error, return_result)\n<\/p>\n<\/blockquote>\n<p>This code is a work in progress, so parts of it may not work. The parts I want to discuss in this blog post, however DO work.<\/p>\n<h1>User under Linux<\/h1>\n<p>A user name, under Linux, will be assigned a user id, a user group, and the user will be a <strong>member<\/strong> of other groups.<\/p>\n<p><strong>\/etc\/passwd<\/strong> contains the users defined on the system:<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-11.png\"><img loading=\"lazy\" decoding=\"async\" width=\"879\" height=\"435\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-11.png\" border=\"0\"><\/a><\/p>\n<p>The entries can be decoded as follows:<\/p>\n<p>user : password : user id (uid) : group id (gid) : full name of the user (GECOS) : user home directory : login shell<\/p>\n<p>Interesting sidenote: storing the password in \/etc\/passwd (in an encrypted form) is optional (in case the password is not stored, an <strong>x<\/strong> is stored here); Since this file is world-readable, many systems opt to use the root-readable \/etc\/shadow instead:<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-12.png\"><img loading=\"lazy\" decoding=\"async\" width=\"901\" height=\"68\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-12.png\" border=\"0\"><\/a><\/p>\n<p>which indeed contains the password.<\/p>\n<p>For picockpit, we get the user name from the JSON settings file, and want to set up everything else accordingly. So we start out with:<\/p>\n<blockquote>\n<p>uid = pwd.getpwnam(user).pw_uid<br \/>\ngid = pwd.getpwnam(user).pw_gid<\/p>\n<\/blockquote>\n<p>\n(Be sure to <em>import pwd<\/em>).<\/p>\n<p>The pwd module allows us to access the \/etc\/passwd file using Python. getpwnam looks up the corresponding line by name of the user, in our case \u201cpi\u201d, or \u201croot\u201d (or whatever you want, whatever is set up in the JSON file).<\/p>\n<p>This will help us obtain the user\u2019s user id (uid) and their group id (gid).<\/p>\n<p> Next we call subprocess.Popen:<\/p>\n<blockquote>\n<p>proc = subprocess.Popen(<br \/>&nbsp;&nbsp;&nbsp;&nbsp; cmd,<br \/>&nbsp;&nbsp;&nbsp;&nbsp; stdout=subprocess.PIPE,<br \/>&nbsp;&nbsp;&nbsp;&nbsp; stderr=subprocess.PIPE,<br \/>&nbsp;&nbsp;&nbsp;&nbsp; preexec_fn=demote(user, uid, gid)<br \/>\n)<\/p>\n<\/blockquote>\n<p>The stdout and stderr are PIPEd, so we can capture them later. <\/p>\n<p>the preexec_fn is set to demote(user, uid, gid). <\/p>\n<h3>Important!<\/h3>\n<p>This is an important bit: <strong>preexec_fn expects a function<\/strong> handle, which it will execute in the context of the new process it is starting (so the process will demote itself!). This function will run before the actual code, <strong>thus we can be sure that the code will execute in the context we want it to execute<\/strong>. <\/p>\n<p>The way we pass demote here, it looks as if the function is being called. <\/p>\n<p>But look at the definition of demote:<\/p>\n<blockquote>\n<p>def demote(user, uid, gid):<br \/><font style=\"background-color: rgb(255, 255, 0);\">&nbsp;&nbsp;&nbsp;&nbsp; def demote_function():<br \/>\n<\/font>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8220;starting&#8221;)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print (&#8216;uid, gid = %d, %d&#8217; % (os.getuid(), os.getgid()))<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print (os.getgroups())<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # initgroups must be run before we lose the privilege to set it!<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.initgroups(user, gid)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.setgid(gid)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # this must be run last<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.setuid(uid)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8220;finished demotion&#8221;)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(&#8216;uid, gid = %d, %d&#8217; % (os.getuid(), os.getgid()))<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print (os.getgroups())<br \/>&nbsp;&nbsp;&nbsp;&nbsp; return demote_function<\/p>\n<\/blockquote>\n<p>This function wraps another function inside itself, which it returns. Using the syntax which is specified above we actually <strong>return a function<\/strong> but are able to pass parameters to it, too.<\/p>\n<p>Kind of like having your cake, and eating it. Nice <img decoding=\"async\" class=\"wlEmoticon wlEmoticon-smile\" style=\"\" alt=\"Smile\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/wlEmoticon-smile-1.png\"><\/p>\n<p>Here is a screenshot, in case WordPress messes with the syntax above:<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-13.png\"><img loading=\"lazy\" decoding=\"async\" width=\"869\" height=\"303\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-13.png\" border=\"0\"><\/a><\/p>\n<p>OK, now I\u2019ll explain the code in this demote function:<\/p>\n<p><strong>os.initgroups: <\/strong><\/p>\n<p>we set up the groups which the process should be a member of (yes, we have to do this \u2013 otherwise the process will only be a member of the default pi group) \u2013 this command expects the user <strong>name<\/strong><\/p>\n<p><a href=\"https:\/\/docs.python.org\/3\/library\/os.html#os.setgid\"><strong>os.setgid:<\/strong><\/a><\/p>\n<p>here we set up the group id for the new process.<\/p>\n<p>There is also a function <a href=\"https:\/\/docs.python.org\/3\/library\/os.html#os.setegid\">os.setegid<\/a>() which is described as \u201cSet the current process\u2019s effective group id.\u201d&nbsp; <\/p>\n<p>What is the difference?<\/p>\n<p>setegid sets the gid in a temporary way, so that the process can return to the original gid later. This is not what we want, since the process we are calling could upgrade it\u2019s rights back up again.<\/p>\n<p><strong><a href=\"https:\/\/docs.python.org\/3\/library\/os.html#os.setuid\">os.setuid(uid):<\/a><\/strong><\/p>\n<p>similarly, there is a <a href=\"https:\/\/docs.python.org\/3\/library\/os.html#os.seteuid\">os.seteuid(euid)<\/a> function, which would allow the executed process to \u201cupgrade back\u201d. So we\u2019re not using that.<\/p>\n<p><\/p>\n<p><strong>The order of these commands is important! <\/strong>If we would set the user id first, we would not have sufficient privileges to set up the groups, for example. So set the user id last.<\/p>\n<p><\/p>\n<p>Sidenote: preexec_fn could be used to set up environment variables and other things, too, to allow for more flexibility. Possibly in a future picockpit-client upgrade, let\u2019s see. <\/p>\n<p>Sidenote 2: the preexec_fn writes to the new process output stream (remember, it is pre-executed in it\u2019s context):<\/p>\n<p>That is why the debug output is sent to the picockpit frontend:<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-14.png\"><img loading=\"lazy\" decoding=\"async\" width=\"380\" height=\"472\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-14.png\" border=\"0\"><\/a><\/p>\n<h2>Ref<\/h2>\n<ul>\n<li><a href=\"https:\/\/stackoverflow.com\/questions\/1770209\/run-child-processes-as-different-user-from-a-long-running-python-process\/6037494#6037494\">https:\/\/stackoverflow.com\/questions\/1770209\/run-child-processes-as-different-user-from-a-long-running-python-process\/6037494#6037494<\/a><\/li>\n<li><a href=\"https:\/\/docs.python.org\/3\/library\/os.html\">https:\/\/docs.python.org\/3\/library\/os.html<\/a><\/li>\n<li><a href=\"https:\/\/docs.python.org\/3\/library\/subprocess.html\">https:\/\/docs.python.org\/3\/library\/subprocess.html<\/a><\/li>\n<\/ul>\n<p>Additional links of interest:<\/p>\n<ul>\n<li><a href=\"https:\/\/stackoverflow.com\/questions\/4789837\/how-to-terminate-a-python-subprocess-launched-with-shell-true\/4791612#4791612\">https:\/\/stackoverflow.com\/questions\/4789837\/how-to-terminate-a-python-subprocess-launched-with-shell-true\/4791612#4791612<\/a><\/li>\n<\/ul>\n<h1>Bonus: Chromium<\/h1>\n<ul>\n<li><b><a href=\"&bull;http:\/\/peter.sh\/experiments\/chromium-command-line-switches\/\">http:\/\/peter.sh\/experiments\/chromium-command-line-switches\/<\/a><\/b><\/li>\n<li><a href=\"https:\/\/www.chromium.org\/developers\/how-tos\/run-chromium-with-flags\">https:\/\/www.chromium.org\/developers\/how-tos\/run-chromium-with-flags<\/a><\/li>\n<\/ul>\n<blockquote>\n<p>chromium-browser &#8211;display=:0 <a href=\"https:\/\/www.picockpit.com\">https:\/\/www.picockpit.com<\/a><\/p>\n<\/blockquote>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-15.png\"><img loading=\"lazy\" decoding=\"async\" width=\"433\" height=\"38\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-15.png\" border=\"0\"><\/a><\/p>\n<p>will allow you to run the Chromium browser from the command line (in the shell). \u2013display=:0 sets the X display to use for Chromium to output to, thus you do not need to set up the appropriate environment variable.<\/p>\n<p><a href=\"&bull;http:\/\/peter.sh\/experiments\/chromium-command-line-switches\/\">Peter.sh<\/a> gives tons of command line switches, which you can use to configure Chromium to your liking (e.g. kiosk mode, etc). <\/p>\n<p>Note that you NEED to run an X server, if you want to display Chromium. On the Raspberry Pi, the easiest way to do is is to run the Raspberry Pi Desktop. <\/p>\n<h1>Looking for professional consulting &amp; development for the Raspberry Pi platform?<\/h1>\n<p><a href=\"https:\/\/pi3g.com\/kontakt\">We offer consulting and development services<\/a> in case you are looking for someone to set up a secure Chromium based Kiosk application for you. <a href=\"https:\/\/pi3g.com\/kontakt\">Get in touch today.<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Sicherheit ist mir bei der Entwicklung des PiCockpit-Clients wichtig. Das Folgende gilt f\u00fcr Linux-Systeme (ist aber wahrscheinlich auf alle Unix-\u00e4hnlichen Systeme anwendbar, einschlie\u00dflich macOS) Python erlaubt es, externe Befehle mit dem Modul subprocess auszuf\u00fchren. import subprocess In der kommenden Version von PiCockpit werden Benutzer in der Lage sein, ihre eigenen Schaltfl\u00e4chen zu erstellen (einfach...<\/p>","protected":false},"author":830,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","_links_to":"","_links_to_target":""},"categories":[402,610],"tags":[795,434,792,794,572,273,793],"class_list":["post-25566","post","type-post","status-publish","format-standard","hentry","category-development","category-python","tag-chromium","tag-picockpit","tag-popen","tag-preexec_fn","tag-python","tag-security","tag-subprocess"],"_links":{"self":[{"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/posts\/25566","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/users\/830"}],"replies":[{"embeddable":true,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/comments?post=25566"}],"version-history":[{"count":1,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/posts\/25566\/revisions"}],"predecessor-version":[{"id":25567,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/posts\/25566\/revisions\/25567"}],"wp:attachment":[{"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/media?parent=25566"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/categories?post=25566"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/tags?post=25566"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}