execução segura de comandos com Python: subprocesso.Popen
A segurança é importante para mim enquanto desenvolvo o picockpit-cliente.
O seguinte aplica-se aos sistemas Linux (mas provavelmente é aplicável a todos os sistemas como o Unix, incluindo o MacOS)
Python permite executar comandos externos usando o módulo de subprocesso.
subprocesso de importação
Na próxima versão do PiCockpit, os usuários poderão criar seus próprios botões (simplesmente editando um arquivo JSON no Pi) a fim de executar comandos.
PiCockpit será enviado com três botões por padrão:
Todos estes comandos requerem privilégios de root. picockpit-cliente, por padrão, roda com privilégios de root no seu sistema.
Mas outros comandos que você quer executar, devem ser executados como usuários menos privilegiados (o padrão é para o usuário "pi"). Na verdade, alguns comandos, como o Chromium Browser irá recusar para correr como raiz.
Em suma, precisamos de uma solução para executar estes processos utilizando um utilizador diferente.
Isto é possível, graças ao preexec_fn parâmetro para popen.
Aqui está o meu código:
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-\u0008u000b-\u000c\u000e-\u001f\ufffe-\uff])' + {\u0008
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))
retornar re.sub(RE_XML_ILLEGAL, "", input_string)def demote(user, uid, gid):
def demote_function():
print("começando")
print ('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
imprimir (os.getgroups())
# initgroups devem ser executados antes de perdermos o privilégio de o definir!
os.initgroups(usuário, gid)
os.setgid(gid)
# isto deve ser executado por último
os.setuid(uid)
print("finished demotion")
print('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
imprimir (os.getgroups())
return demote_functiondef run_cmd(cmd, cmd_def):
return_code = 0
return_error = ""
return_result = ""
timeout = Nenhum
usuário = "pi
se "timeout" em cmd_def:
if isinstance(cmd_def["timeout"], int):
se cmd_def["timeout"] > 0:
timeout = cmd_def["timeout"]
se "usuário" em cmd_def:
if isinstance(cmd_def["user"], str):
usuário = cmd_def["usuário"]
tente:
print("picontrol :: configuração dos direitos do utilizador")
uid = pwd.getpwnam(user).pw_uid
gid = pwd.getpwnam(user).pw_gid
print("picontrol :: utilizador solicitado " + utilizador + " uid " + str(uid) + " gid " + str(gid))
print("picontrol :: comando de arranque ... ")
imprimir(cmd)
proc = subprocesso.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=demote(user, uid, gid)
)
# TODO: lidar com timeout de forma diferente
# timeout=timeout)
print("picontrol :: APÓS o subprocesso.Popen")
excepto FileNotFoundError:
return_code = errno.ENOENT
return_error = "Comando não encontrado no seu sistema " + " ".join(cmd)
# TODO: isto alguma vez será realmente chamado?
exceto subprocesso.TimeoutExpirado:
return_code = errno.ETIME
return_error = "Timout de comando expirado (" + str(cmd_def["timeout"]) + " seg)"
exceto Exceção como e:
se hasattr(e, 'errno'):
return_code = e.errno
senão..:
return_code = 1
se hasattr(e, 'mensagem'):
return_error = str(e.message)
senão..:
return_error = str(e)
senão..:
O processo # pode ser executado normalmente, sem exceções na inicialização
process_running = Verdadeiro
durante a execução do_processo:
se proc.poll() não for Nenhuma:
process_running = Falso
se self.stop_flag.is_set():
print("stop flag received in process_running, setting process_running to false")
process_running = Falso
# e também fazer passos para a terminação activa depois disso.
# meio segundo de resolução
tempo.de sono(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"))retorno (código_retorno, erro_retorno, retorno_resultado)
Este código é um trabalho em andamento, portanto partes dele podem não funcionar. As partes que eu quero discutir neste post do blog, no entanto, funcionam.
Usuário sob Linux
Um nome de usuário, sob o Linux, será atribuído a um user id, um grupo de usuários, e o usuário será um membro de outros grupos.
/etc/passwd contém os usuários definidos no sistema:
As entradas podem ser descodificadas da seguinte forma:
usuário : senha : user id (uid) : group id (gid) : nome completo do usuário (GECOS) : diretório home do usuário : shell de login
sidenote interessante: o armazenamento da senha em /etc/passwd (de forma criptografada) é opcional (caso a senha não seja armazenada, um x é armazenado aqui); Como este arquivo é legível mundialmente, muitos sistemas optam por usar o /etc/shadow legível pela raiz em seu lugar:
que de facto contém a senha.
Para o picockpit, obtemos o nome de utilizador do ficheiro de definições JSON, e queremos configurar tudo o resto de acordo. Por isso, começamos com:
uid = pwd.getpwnam(user).pw_uid
gid = pwd.getpwnam(user).pw_gid
(Não se esqueça de dívida pública de importação).
O módulo pwd permite-nos aceder ao ficheiro /etc/passwd usando o Python. getpwnam procura na linha correspondente pelo nome do utilizador, no nosso caso "pi", ou "root" (ou o que quiser, o que estiver configurado no ficheiro JSON).
Isto nos ajudará a obter o id do usuário (uid) e o id do seu grupo (gid).
A seguir chamamos de subprocesso. Popen:
proc = subprocesso.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=demote(user, uid, gid)
)
As stdout e stderr são PIPEd, para que possamos capturá-las mais tarde.
o preexec_fn está definido para demote(user, uid, gid).
Importante!
Isto é uma parte importante: preexec_fn espera uma função que vai executar no contexto do novo processo que está a iniciar (por isso o processo vai-se desmobilizar!). Esta função será executada antes do código real, assim podemos ter certeza de que o código será executado no contexto em que queremos que ele execute.
A forma como passamos aqui, parece que a função está a ser chamada.
Mas olha para a definição de demote:
def demote(user, uid, gid):
def demote_function():
print("começando")
print ('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
imprimir (os.getgroups())
# initgroups devem ser executados antes de perdermos o privilégio de o definir!
os.initgroups(usuário, gid)
os.setgid(gid)
# isto deve ser executado por último
os.setuid(uid)
print("finished demotion")
print('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
imprimir (os.getgroups())
return demote_function
Esta função envolve outra função dentro de si mesma, a qual retorna. Usando a sintaxe que é especificada acima, nós realmente devolver uma função mas são capazes de lhe passar parâmetros, também.
Como ter o teu bolo e comê-lo. Bom
Aqui está uma captura de tela, para o caso de o WordPress mexer com a sintaxe acima:
Muito bem, agora vou explicar o código nesta função de despromover:
os.initgroups:
configuramos os grupos dos quais o processo deve ser membro (sim, temos que fazer isso - caso contrário o processo será apenas um membro do grupo pi padrão) - este comando espera que o usuário nome
aqui montamos a identificação do grupo para o novo processo.
Há também uma função os.setegid() que é descrito como "Definir o ID do grupo efetivo do processo atual".
Qual é a diferença?
setegid define o gid de uma forma temporária, para que o processo possa voltar ao gid original mais tarde. Isto não é o que nós queremos, uma vez que o processo que estamos chamando poderia atualizar seus direitos de volta.
Da mesma forma, há um os.seteuid(euid) o que permitiria que o processo executado "atualizasse de volta". Portanto, não vamos usar isso.
A ordem destes comandos é importante! Se definirmos a identificação do usuário primeiro, não teremos privilégios suficientes para configurar os grupos, por exemplo. Portanto, defina o user id por último.
Sidenote: preexec_fn poderia ser usado para configurar variáveis de ambiente e outras coisas, também, para permitir mais flexibilidade. Possivelmente em uma futura atualização picockpit-cliente, vamos ver.
Sidenote 2: o preexec_fn escreve para o novo fluxo de saída do processo (lembre-se, ele é pré-executado em seu contexto):
É por isso que a saída de depuração é enviada para o front-end do picockpit:
Ref
- 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
Links de interesse adicionais:
Bónus: Crómio
- http://peter.sh/experiments/chromium-command-line-switches/
- https://www.chromium.org/developers/how-tos/run-chromium-with-flags
cromium-browser -display=:0 https://www.picockpit.com
permitirá que você execute o navegador Chromium a partir da linha de comando (na shell). -display=:0 define o display X a ser usado para o Chromium sair, assim você não precisa configurar a variável de ambiente apropriada.
Peter.sh dá toneladas de chaves de linha de comando, que você pode usar para configurar o Chromium a seu gosto (por exemplo, modo quiosque, etc).
Note que você PRECISA executar um servidor X, se você quiser exibir o Chromium. No Raspberry Pi, a maneira mais fácil de fazer é rodar o Raspberry Pi Desktop.
Procurando consultoria e desenvolvimento profissional para a plataforma Raspberry Pi?
Nós oferecemos serviços de consultoria e desenvolvimento no caso de estar à procura de alguém para montar uma aplicação Kiosk segura à base de Crómio para si. Entre em contato hoje.