exécution sécurisée des commandes avec Python : subprocess.Popen
La sécurité est importante pour moi lors du développement du picockpit-client.
Ce qui suit s'applique aux systèmes Linux (mais s'applique probablement à tous les systèmes de type Unix, y compris macOS)
Python permet d'exécuter des commandes externes à l'aide du module subprocess.
Importation de sous-processus
Dans la prochaine version de PiCockpit, les utilisateurs pourront créer leurs propres boutons (en modifiant simplement un fichier JSON sur le Pi) afin d'exécuter des commandes.
PiCockpit sera livré avec trois boutons par défaut :
Ces commandes requièrent toutes les privilèges de root. picockpit-client, par défaut, s'exécute avec les privilèges de root sur votre système.
Mais d'autres commandes que vous voulez exécuter, devraient être exécutées par des utilisateurs moins privilégiés (nous utilisons par défaut l'utilisateur "pi"). En fait, certaines commandes, comme le navigateur Chromium, vont refuser pour être exécuté en tant que root.
En bref, nous avons besoin d'une solution pour exécuter ces processus en utilisant un utilisateur différent.
Cela est possible, grâce à la preexec_fn paramètre pour popen.
Voici mon code :
def thread_run(self) :
def sanitize(input_string) :
#https://maxharp3r.wordpress.com/2008/05/15/pythons-minidom-xml-and-illegal-unicode-characters/
RE_XML_ILLEGAL = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \o
u'|' + \
u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])’ % \
(chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff),
chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff),
chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff))
return re.sub(RE_XML_ILLEGAL, "", input_string)def demote(utilisateur, uid, gid) :
def demote_function() :
print("starting")
print ('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
print (os.getgroups())
# initgroups doit être exécuté avant que nous perdions le privilège de le définir !
os.initgroups(user, gid)
os.setgid(gid)
# cela doit être exécuté en dernier
os.setuid(uid)
print("rétrogradation terminée")
print('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
print (os.getgroups())
retour de la fonction démotedef run_cmd(cmd, cmd_def) :
return_code = 0
return_error = ""
return_result = ""
timeout = Aucun
utilisateur = "pi"
si "timeout" dans cmd_def :
si isinstance(cmd_def["timeout"], int) :
si cmd_def["timeout"] > 0 :
timeout = cmd_def["timeout"]
si "user" dans cmd_def :
si isinstance(cmd_def["user"], str) :
utilisateur = cmd_def["utilisateur"]
essayez :
print("picontrol : : configuration des droits de l'utilisateur")
uid = pwd.getpwnam(user).pw_uid
gid = pwd.getpwnam(user).pw_gid
print("picontrol : : requested user " + user + " uid " + str(uid) + " gid " + str(gid))
print("picontrol : : starting command ... ")
print(cmd)
proc = subprocess.Popen(
cmd,
stdout=sous-processus.PIPE,
stderr=sous-processus.PIPE,
preexec_fn=demote(user, uid, gid)
)
# TODO : gérer le délai d'attente différemment
# timeout=timeout)
print("picontrol : : AFTER subprocess.Popen")
sauf FileNotFoundError :
return_code = errno.ENOENT
return_error = "Commande non trouvée sur votre système " + " ".join(cmd)
# TODO : est-ce que cela sera un jour appelé ?
sauf subprocess.TimeoutExpired :
return_code = errno.ETIME
return_error = "Command timeout expired (" + str(cmd_def["timeout"]) + " sec)"
sauf Exception comme e :
si hasattr(e, 'errno') :
return_code = e.errno
autre :
return_code = 1
si hasattr(e, 'message') :
return_error = str(e.message)
autre :
return_error = str(e)
autre :
Le processus # peut s'exécuter normalement, sans exception au démarrage.
process_running = True
while process_running :
si proc.poll() n'est pas None :
process_running = Faux
si self.stop_flag.is_set() :
print("drapeau d'arrêt reçu dans process_running, mettant process_running à false")
process_running = Faux
# et faire aussi les étapes pour la terminaison active après cela.
# une demi-seconde de résolution
temps.sleep(0.5)
return_code = proc.returncode
return_error = sanitize(proc.stderr.read().decode('UTF-8').rstrip("\r\n"))
return_result = sanitize(proc.stdout.read().decode('UTF-8').rstrip("\r\n"))return (return_code, return_error, return_result)
Ce code est un travail en cours, donc certaines parties peuvent ne pas fonctionner. Les parties dont je veux parler dans cet article de blog, cependant, fonctionnent.
Utilisateur sous Linux
Un nom d'utilisateur, sous Linux, se verra attribuer un identifiant, un groupe d'utilisateurs, et l'utilisateur sera un membre d'autres groupes.
/etc/passwd contient les utilisateurs définis sur le système :
Les entrées peuvent être décodées comme suit :
utilisateur : mot de passe : identifiant de l'utilisateur (uid) : identifiant du groupe (gid) : nom complet de l'utilisateur (GECOS) : répertoire d'origine de l'utilisateur : shell de connexion
Remarque intéressante : le stockage du mot de passe dans /etc/passwd (sous une forme cryptée) est facultatif (dans le cas où le mot de passe n'est pas stocké, une x est stocké ici) ; Comme ce fichier est lisible par le monde entier, de nombreux systèmes choisissent d'utiliser le fichier /etc/shadow lisible par les racines à la place :
qui contient effectivement le mot de passe.
Pour picockpit, nous obtenons le nom d'utilisateur à partir du fichier de paramètres JSON, et nous voulons configurer tout le reste en conséquence. Donc nous commençons avec :
uid = pwd.getpwnam(user).pw_uid
gid = pwd.getpwnam(user).pw_gid
(Veillez à Importer pwd).
Le module pwd nous permet d'accéder au fichier /etc/passwd en utilisant Python. getpwnam recherche la ligne correspondante par le nom de l'utilisateur, dans notre cas "pi", ou "root" (ou ce que vous voulez, ce qui est défini dans le fichier JSON).
Cela nous permettra d'obtenir l'identifiant de l'utilisateur (uid) et son identifiant de groupe (gid).
Ensuite, nous appelons subprocess.Popen :
proc = subprocess.Popen(
cmd,
stdout=sous-processus.PIPE,
stderr=sous-processus.PIPE,
preexec_fn=demote(user, uid, gid)
)
Les stdout et stderr sont PIPEd, afin que nous puissions les capturer plus tard.
le preexec_fn est défini comme demote(user, uid, gid).
Important !
C'est un élément important : preexec_fn attend une fonction qu'il exécutera dans le contexte du nouveau processus qu'il démarre (pour que le processus se rétrograde lui-même !). Cette fonction sera exécutée avant le code réel, ainsi nous pouvons être sûrs que le code s'exécutera dans le contexte que nous voulons qu'il exécute.
La façon dont nous passons la rétrogradation ici, on dirait que la fonction est appelée.
Mais regardez la définition de "rétrograder" :
def demote(utilisateur, uid, gid) :
def demote_function() :
print("starting")
print ('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
print (os.getgroups())
# initgroups doit être exécuté avant que nous perdions le privilège de le définir !
os.initgroups(user, gid)
os.setgid(gid)
# cela doit être exécuté en dernier
os.setuid(uid)
print("rétrogradation terminée")
print('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
print (os.getgroups())
retour de la fonction démote
Cette fonction enveloppe une autre fonction en son sein, qu'elle renvoie. En utilisant la syntaxe spécifiée ci-dessus, nous avons en fait retourner une fonction mais peuvent également lui passer des paramètres.
C'est un peu comme avoir son gâteau et le manger. Nice
Voici une capture d'écran, au cas où WordPress s'embrouillerait avec la syntaxe ci-dessus :
OK, maintenant je vais expliquer le code de cette fonction de rétrogradation :
os.initgroups :
nous définissons les groupes dont le processus doit être membre (oui, nous devons le faire - sinon le processus sera seulement membre du groupe pi par défaut) - cette commande attend l'utilisateur nom
ici nous configurons l'id de groupe pour le nouveau processus.
Il existe également une fonction os.setegid() qui est décrit comme "Définir l'identifiant de groupe effectif du processus actuel".
Quelle est la différence ?
setegid définit le gid de manière temporaire, de sorte que le processus puisse revenir au gid original plus tard. Ce n'est pas ce que nous voulons, puisque le processus que nous appelons pourrait remettre ses droits à jour.
De même, il existe un os.seteuid(euid) qui permettrait au processus exécuté d'effectuer une "remise à niveau". Nous ne l'utilisons donc pas.
L'ordre de ces commandements est important ! Si nous définissions l'identifiant de l'utilisateur en premier, nous n'aurions pas les privilèges suffisants pour configurer les groupes, par exemple. Il faut donc définir l'identifiant de l'utilisateur en dernier.
Note complémentaire : preexec_fn pourrait être utilisé pour configurer des variables d'environnement et d'autres choses, aussi, pour permettre plus de flexibilité. Peut-être dans une future mise à jour de picockpit-client, voyons voir.
Remarque 2 : le preexec_fn écrit dans le flux de sortie du nouveau processus (rappelez-vous, il est pré-exécuté dans son contexte) :
C'est pourquoi la sortie de débogage est envoyée au frontal picockpit :
Réf.
- https://stackoverflow.com/questions/1770209/run-child-processes-as-different-user-from-a-long-running-python-process/6037494#6037494
- https://docs.python.org/3/library/os.html
- https://docs.python.org/3/library/subprocess.html
Autres liens d'intérêt :
Bonus : Chrome
- http://peter.sh/experiments/chromium-command-line-switches/
- https://www.chromium.org/developers/how-tos/run-chromium-with-flags
chromium-browser -display=:0 https://www.picockpit.com
vous permettra d'exécuter le navigateur Chromium à partir de la ligne de commande (dans le shell). -display=:0 définit l'affichage X à utiliser pour la sortie de Chromium, vous n'avez donc pas besoin de configurer la variable d'environnement appropriée.
Peter.sh donne des tonnes de commutateurs de ligne de commande, que vous pouvez utiliser pour configurer Chromium à votre goût (par exemple, le mode kiosque, etc).
Notez que vous DEVEZ exécuter un serveur X, si vous voulez afficher Chromium. Sur le Raspberry Pi, la façon la plus simple de procéder est d'exécuter le Raspberry Pi Desktop.
Vous recherchez des services professionnels de conseil et de développement pour la plateforme Raspberry Pi ?
Nous offrons des services de conseil et de développement si vous cherchez quelqu'un pour mettre en place pour vous une application kiosque sécurisée basée sur Chromium. Prenez contact avec nous dès aujourd'hui.