OpenCyber Academy
Engenharia · Capítulo 4 · Português
Código do curso: OCA-135Capítulo 04: OCA-135-C004

Capítulo 4 — Programação I

Ensino gratuito e aberto para quem deseja proteger pessoas e tecnologias. Não é preciso criar uma conta para estudar ou consultar as respostas.

Mapa da aula

Aula 04 publicada. Mapa, exemplo, EX010.

flowchart TD
  N0["4.1 Programas como instruções precisas"]
  N1["4.2 Estruturas de dados"]
  N2["4.3 Entrada não confiável"]
  N3["4.5 Testes"]
  N4["Síntese"]
  N0 --> N1 --> N2 --> N3 --> N4
  N4 --> EX["Exemplo resolvido"]
  EX --> Q["Exercício EX010"]

Desenho lógico

flowchart LR
  N0["4.1 Programas como instruções precisas"]
  N1["4.2 Estruturas de dados"]
  N2["4.3 Entrada não confiável"]
  N3["4.5 Testes"]
  N4["Síntese"]
  N0 --> N1 --> N2 --> N3 --> N4 --> Q["EX010"]
[4.1 Programas como instruções precisas]
    |
    v
[4.2 Estruturas de dados]
    |
    v
[4.3 Entrada não confiável]
    |
    v
[4.5 Testes]
    |
    v
[Síntese]
    |
    v
[Exemplo resolvido] --> [Exercício EX010]

Objetivos de aprendizagem

Você vai (1) escrever programas Python pequenos e corretos com funções, laços e estruturas de dados; (2) tratar entradas não confiáveis com segurança; (3) testar código com asserções; e (4) ler os laboratórios do livro e modificá-los.

4.1 Programas como instruções precisas

Um programa diz exatamente o que o computador faz. Variáveis guardam valores; funções empacotam passos; laços repetem; condicionais escolhem. Código legível usa nomes claros e funções pequenas. Os laboratórios deste livro usam apenas a biblioteca padrão do Python, para rodarem em qualquer lugar. A precisão corta dos dois lados: o computador faz o que o código diz, não o que o autor quis dizer. Um laço que para um item antes ou uma condição escrita com 'or' em vez de 'and' produz resultados errados sem nenhum aviso. Sam lê cada laboratório linha por linha antes de executá-lo e prevê a saída, de modo que uma surpresa no resultado impresso aponta uma lacuna no seu entendimento.

4.2 Estruturas de dados

Listas guardam sequências; dicionários associam chaves a valores; conjuntos guardam itens únicos. Um inventário de modelos é naturalmente uma lista de dicionários; uma lista de permissões de uma política é naturalmente um conjunto. Escolher a estrutura certa deixa o código mais curto e mais seguro: a pertinência a um conjunto é um teste claro e rápido para "esta ferramenta é permitida?". Uma lista também funcionaria como lista de permissões, mas verificar a pertinência nela significa percorrer todos os itens, e duplicatas podem entrar sem que ninguém perceba. A estrutura errada também pode esconder um bug de segurança: se as ferramentas permitidas forem guardadas como uma única string separada por vírgulas, uma verificação como 'search' in allowed aceitaria também uma ferramenta chamada 'research'.

4.3 Entrada não confiável

Quaisquer dados de fora do seu programa — arquivos, mensagens de rede, saídas de modelos — são não confiáveis. Nunca os passe para eval, comandos de shell ou consultas a bancos de dados sem validação estrita. Isso se aplica à IA: o OWASP LLM 2025 lista o tratamento inadequado de saídas (LLM05) porque aplicações frequentemente executam ou renderizam diretamente a saída do modelo. Faça o parsing com parsers estritos (por exemplo, json.loads), valide tipos e faixas e rejeite campos desconhecidos. Rejeitar campos desconhecidos importa porque sistemas a jusante podem agir sobre campos que o validador nunca examinou. Um parser estrito os recusa de imediato.

4.4 Erros como informação

Quando o código falha, ele deve falhar fechado e explicar o motivo. Um parser que descarta silenciosamente um campo desconhecido pode deixar passar um campo que o sistema a jusante respeita; um parser que rejeita o objeto inteiro com uma mensagem clara é mais seguro e mais fácil de depurar. Registre a rejeição com contexto suficiente para investigar (hora, origem, motivo), mas sem copiar conteúdo sensível para os logs. Exceções são o mecanismo normal do Python: lance ValueError com uma mensagem e deixe quem chamou decidir se tenta de novo, consulta um humano ou para.

4.5 Testes

Um assert declara o que precisa ser verdade. Testes unitários chamam funções com entradas escolhidas e conferem as saídas, inclusive casos-limite (listas vazias, valores muito grandes, texto malformado). Testes tornam seguras as mudanças futuras. Para código de segurança, os testes mais valiosos são os que deveriam falhar: uma ordem de serviço com um campo extra, uma prioridade escrita de outro jeito, um identificador de ativo contendo pontuação. Cada rejeição esperada é escrita como teste, de modo que uma mudança futura que afrouxe a validação quebre o build em vez de chegar à produção. Sam guarda como caso de teste permanente toda entrada malformada que já causou um problema.

4.6 Exemplo resolvido (caso sintético da NRS)

Sam escreve uma função que faz o parsing da ordem de serviço proposta pelo Wrench (JSON) antes que ela chegue ao sistema de ordens de serviço. O programa abaixo imprime a ordem válida e depois rejected: bad asset id. A saída do modelo é tratada como dado a validar, nunca como código. O identificador do ativo é rejeitado porque o ponto e vírgula e o espaço falham na verificação alfanumérica, então um texto que parece um comando de shell nunca chega ao sistema de ordens de serviço. A função nunca exige um resumo, então uma ordem sem resumo passaria; Sam acrescenta essa verificação em seguida. Na produção, Sam substitui os asserts por exceções explícitas, pelo motivo dado entre os erros abaixo.

import json
ALLOWED = {"asset_id", "priority", "summary"}
def parse_order(text):
    d = json.loads(text)
    assert isinstance(d, dict) and set(d) <= ALLOWED, "unexpected fields"
    assert d.get("priority") in {"low", "medium", "high"}, "bad priority"
    assert isinstance(d.get("asset_id"), str) and d["asset_id"].isalnum(), "bad asset id"
    return d
print(parse_order('{"asset_id": "AX12", "priority": "high", "summary": "bearing hot"}'))
try:
    parse_order('{"asset_id": "AX12; rm", "priority": "high"}')
except AssertionError as e:
    print("rejected:", e)

4.7 Erros e limites

Três erros são comuns: executar a saída do modelo como código ou comandos de shell; uma validação que só confere os caminhos felizes; e usar asserts como única verificação em código de produção (eles podem ser desativados com python -O; ali, use exceções explícitas). Cada erro tem um sinal reconhecível na revisão de código. Chamadas a eval, exec ou subprocess com uma string montada a partir da saída do modelo são a primeira coisa a procurar. Validadores sem testes para entradas rejeitadas provavelmente nunca foram experimentados com dados ruins. Asserts que protegem condições de segurança devem virar if-statements que lançam uma exceção nomeada, porque uma execução otimizada na produção pularia a verificação por completo, sem nenhuma mensagem.

Exercícios

Síntese

Programar para a segurança de IA começa com um hábito: tratar tudo o que vem de fora — inclusive a saída do modelo — como dado não confiável, validá-lo estritamente e testar os limites.

Continue estudando — capítulo 04 de 50

← Capítulo 03 (rascunho) · Índice de capítulos do curso · Capítulo 05 (rascunho) →

Respostas comentadas

EX010. TOOLS = {"search", "summarise"}; def allowed(tool): return tool in TOOLS com assert allowed("search") and not allowed("email").

EX011. eval executa o texto como Python, então um texto influenciado por um atacante (por exemplo, via injeção de prompt) vira execução arbitrária de código. OWASP LLM05 Improper Output Handling.

EX012. JSON inválido; objeto vazio; campo extra; asset_id com pontuação ou que não seja string; prioridade fora do conjunto; resumo extremamente longo.