Seguranca do caminho — Arduino e IoT — semana 9 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 9 · Seguranca do caminho — Material de Apoio Arduino

Semana 9 de 16· 2o trimestre · 01/05 a 04/09

Seguranca do caminho

O caminho fica exposto na rede da escola. O que precisa de protecao.

Aula 1 — Segredo, rede e o que não va para o git

Objetivos

  • Separar, em dois arquivos, o nome da variável de configuração do valor dela, e dizer qual dos dois vai para o git.
  • Reproduzir na máquina o caso do segredo commitado e apagado, e mostrar que o histórico continua com a senha.
  • Escrever o .gitignore que protege a credencial da placa e a do Node, e dizer por que ele não desfaz commit antigo.
  • Conferir o estado do WiFi sem nunca imprimir a senha, e dizer o que um MAC address prova e o que não prova.
  • Decidir, para a rede da escola, se a placa entra em rede aberta, isolada ou com senha forte.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 computador com git instalado e Node.js 20
  • Roteador com a rede de teste do professor, e senha WPA2 ou WPA3
  • Folha de papel por dupla, com as duas colunas: o que versiona e o que não versiona
  • Projetor, para o terminal do git e o monitor serial da placa juntos

Conceitos

Nome vai, valor não

A regra da aula cabe em uma frase, e o professor escreve na lousa antes de qualquer código: no git vai o nome da variável, nunca o valor.

O que vai para o git é o *mecanismo*: qual variável o programa lê, de onde ela vem, e o que fazer quando ela não existe. O que não vai é o *valor*: a senha do banco, a chave que a placa apresenta ao servidor, a senha do WiFi.

Isso vale para os dois lados do cabo, e a distinção é o que a aula ensina. O caso que dá nome ao problema é a senha de WiFi em texto: até o dia 8, ela estava escrita no .ino em duas linhas, e o .ino é um arquivo versionado. Uma senha no código não vira segredo pelo fato de o código ser privado: vira segredo assim que o código for clonado, e o primeiro clone é o do colega que pede ajuda.

LadoOnde o nome é declaradoOnde o valor moraO que nunca vai para o git
Nodeconfig.exemplo.js, versionado.env da máquina, não versionadoo valor da senha do banco
Placasecrets.h na pasta do sketch, não versionadodentro do secrets.ha senha do WiFi e a chave de API

O arquivo versionado do Node e o secrets.h da placa têm a mesma função, e é por isso que são o mesmo mecanismo com nome diferente. O aluno que entende o do Node entende o da placa em dois minutos, e o caminho inverso também vale.

O porquê de o valor ser um segredo tem um nome: credencial exposta é o estado em que a credencial saiu do lugar onde deveria estar. Uma credencial exposta não é uma credencial fraca: é uma credencial que todo mundo que leu o repositório pode usar. No caso da escola, "todo mundo" inclui quem entrou no repositório há seis meses e já saiu da escola.

A senha de API e o token de API são o mesmo mecanismo com nomes diferentes, e os dois aparecem no código como uma variável cujo valor vem de fora. O que o professor nunca faz, em nenhuma aula do curso, é escrever o valor de verdade no arquivo versionado. Todo valor que aparece no material é fictício e está comentado como tal, e isso não éOpenedcourtesy: é a regra.

Apagar o arquivo não apaga o commit

Este é o item que o professor demonstra com git de verdade, e a demonstration dura três minutos. O roteiro é o seguinte e é o roteiro real de um projeto de aluno:

  1. O arquivo de credencial é criado e vai para o primeiro commit, porque ninguém tinha pensado em .gitignore ainda.
  2. Alguém descobre e escreve o .gitignore num commit seguinte.
  3. O arquivo é apagado do disco, porque o aluno entendeu o recado.

O resultado: o arquivo sumiu do diretório de trabalho, e a senha continua no histórico, legível, com git show. E a razão é técnica, não política: o conteúdo do repositório é um banco de dados de objetos, e apagar o arquivo de trabalho não reescreve o banco. Um objeto do banco não se apaga apagando o caminho: ou se reescreve a história inteira, e aí todo mundo que já clonou precisa refazer o clone, ou a senha muda e o valor antigo morre sozinho.

A consequência prática é a que o professor escreve na lousa: não versionar é uma decisão que se toma antes do primeiro git add, e não depois. Depois do commit, o .gitignore só protege o futuro.

Rede da escola: o que a placa é e o que a rede exige

A placa é um cliente: ela se associa a um ponto de acesso que tem senha. Redes WPA2 e WPA3 são o normal de uma escola com política de acesso, e a senha de WiFi forte é a diferença entre uma rede de aula e uma rede pública.

O caso que o professor pede para descrever é o do access point aberto: ponto de acesso sem senha, onde qualquer um dentro do alcance se associa. Uma placa com rede aberta não é "sem segurança" no sentido abstrato: é uma placa que qualquer pessoa com um celular da mesma sala pode associar, ver o tráfego e tentar enviar leitura para o servidor do projeto. E a leitura enviada por alguém que não é a placa é o que o dia 9 aula 2 resolve com a autenticação.

A defesa não é a senha sozinha: é isolar a rede. Uma rede local de teste, com a placa e o servidor no mesmo segmento e sem rota para a internet, já elimina o grosso do problema sem depender de ninguém configurar nada. A placa só fala com o servidor do projeto; se alguém tentar mandar outro JSON, o dia 9 aula 2 barra.

Cliente autorizado é o termo que fecha a conta: é a placa que o servidor reconhece, e ele existe porque isolar a rede não basta. Isolar a rede impede que alguém de fora entre; o cliente autorizado impede que alguém de dentro mande leitura em nome de outra placa. São defesas em camadas, e cada uma cobre um erro diferente. O id_dispositivo é o nome do cliente autorizado no banco, e a chave que ele apresenta é a senha desse acesso.

O MAC address entra aqui porque é o único identificador que a placa tem e é mal interpretado por todo mundo. Ele é um endereço, não uma chave:

  • ele é fixo de fábrica e visível a qualquer pessoa na mesma rede, inclusive no log do roteador;
  • ele não autentica nada: quem copia o MAC de uma placa pode se passar por ela em uma rede mal configurada;
  • ele identifica qual dispositivo é, e é por isso que é a primeira coluna que se filtra no log do roteador quando alguém pergunta "quem está nesse endereço".

Confundir endereço com chave é o erro conceitual da aula, e o professor o escreve na lousa como par: MAC é endereço, token é chave, senha é senha. Nenhum dos três é o outro.

Conferir a configuração sem mostrar a senha

Nem todo caso de depuração exige ver o valor. A placa consegue responder "a senha chegou?" sem responder "qual é a senha", e a diferença entre as duas perguntas é o que separa depuração de vazamento.

A função que faz isso mede o tamanho do valor e o número de caracteres não nulos, e imprime só isso. É a mesma técnica que o strlen do dia 1 do 1o trimestre e o keyed do DHT11, e ela resolve o caso que mais trava aluno: "a senha mudou e a placa não entra mais". Com o tamanho, o aluno descobre se o valor chegou vazio, se chegou pela metade, e se o secrets.h nem foi lido.

O professor pede esse teste no item 4 da atividade porque ele é o que separa as duas hipóteses mais comuns do non connection: senha errada e arquivo de segredo não encontrado. A primeira tem tamanho correto e valor errado. A segunda tem tamanho zero.

Atividade

Montagem:

  • Nenhuma montagem nova. A placa fica com o WiFi desligado, e o professor explica que o ponto da aula é o segredo, não o sinal.
  • Computador com o projeto do aluno aberto, na pasta do git.
  1. Rode o script da resolução e cole no caderno a saída do git show, com a linha da senha circulada. Escreva em uma frase por que apagar o arquivo não resolveu.
  2. Escreva no caderno a tabela das duas colunas para o seu projeto: nome da variável, onde o valor mora, e se vai para o git. Inclua pelo menos a senha do banco, a chave de API e a senha do WiFi da placa.
  3. Escreva o .gitignore do seu projeto e rode git status. O arquivo de credencial aparece na lista de ignorados? Se aparece, por que? Se não aparece, o que precisa acontecer com o arquivo?
  4. Grave na placa o sketch da resolução e leia a saída. Anote o tamanho que a função de descrição imprime para a senha de cada um dos três passos da cadeia de configuração. Algum deles é zero? O que isso diz sobre a sua máquina?
  5. Ligue a placa na rede de teste com senha WPA2 e rode WiFi.begin. A placaHdá tempo de assoc? Em quanto tempo? O que muda se a senha estiver errada: só o tempo, ou também o resultado?
  6. Rode WiFi.macAddress e anote o valor. Depois, no terminal, rode arp -a e procure esse valor na saída. O que o endereço da placa aparece na tabela do roteador como: endereço ou chave? Escreva a diferença.
  7. Escreva, em duas linhas, o que você faria de diferente se a sua placa fosse para um corredor da escola, sem ninguém para reiniciar, e o que no código de hoje impede isso.

Nota: 12 pontos. Critério de fim: o .gitignore escrito e o git status mostrando o arquivo de credencial na lista de ignorados, e a saída do git show com a linha da senha circulada no caderno.

Resolucao

Do lado do Node, o script faz em três minutos o que a aula inteira vai explicar. Este foi executado nesta máquina:

// dia 9 aula 1, lado do Node: o que vai para o git e o que nao vai.
const fs = require('fs');
const path = require('path');

// ---------------------------------------------------------------------------
// O QUE ISTO FAZ: cria um repositorio de mentira numa pasta temporaria, faz
// o commit de um arquivo que CONTEM um segredo e depois "apaga" o arquivo.
// O git log e a prova de que apagar nao resolve. E o que o professor
// mostra na lousa depois de dizer "apaguei e continua la".
// ---------------------------------------------------------------------------

const PASTA = '/tmp/git-da-aula';
const ARQUIVO_COM_SEGREDO = path.join(PASTA, 'config.local.js');
const ARQUIVO_COM_MECHANISMO = path.join(PASTA, 'config.exemplo.js');
const GITIGNORE = path.join(PASTA, '.gitignore');

function sh(comando) {
  return require('child_process').execSync(comando, { cwd: PASTA, encoding: 'utf8' }).trim();
}

fs.rmSync(PASTA, { recursive: true, force: true });
fs.mkdirSync(PASTA, { recursive: true });

// O arquivo de credencial: tem o VALOR. Este e o que nao pode ir para o git.
fs.writeFileSync(ARQUIVO_COM_SEGREDO,
// NAO VERSIONADO. Este arquivo fica na sua maquina e nunca entra no commit.
module.exports = {
  usuario: 'aula',
  senhaDoBanco: 'valor-ficticio-da-aula',
  chaveDaPlaca: 'valor-ficticio-da-aula-09',
};
);

// O arquivo de exemplo: tem o MECANISMO e o NOME da variavel, sem valor.
// E este que vai para o git, e e este que o colega vai ler para saber
// que variavel precisa existir na maquina dele.
fs.writeFileSync(ARQUIVO_COM_MECHANISMO,
// VERSIONADO. So o nome da variavel e o mecanismo. Nenhum valor.
module.exports = {
  // variaveis de ambiente que o processo precisa encontrar:
  usuario:     process.env.DB_USER,    // quem entra no banco
  senhaDoBanco: process.env.DB_PASS,   // senha do banco
  chaveDaPlaca: process.env.CHAVE_API, // chave que a placa apresenta ao servidor
  // onde a credencial de verdade mora: no .env da maquina, no secrets.h da placa
};
);

// A ordem importa, e e a ordem real de um projeto de aluno: primeiro o
// arquivo de credencial entra, sem nenhum .gitignore na pasta. Depois alguem
// descobre o problema e escreve o .gitignore num commit seguinte. Por isso o
// .gitignore e criado DEPOIS do primeiro commit, com o arquivo no git add.
sh('git init -q');
sh('git config user.email [email protected]');
sh('git config user.name "professor da aula"');
sh('git add -f config.local.js config.exemplo.js && git commit -q -m "config do projeto"');
fs.writeFileSync(path.join(PASTA, '.gitignore'), 'config.local.js\n.env\nnode_modules/\n');
sh('git add .gitignore && git commit -q -m "ignora o que nao pode ir para o git"');

console.log('--- os dois commits ---');
console.log(sh('git log --oneline'));
console.log('');
console.log('--- o que o primeiro commit levou junto ---');
console.log(sh('git show --stat --oneline HEAD~1 | head -6'));
console.log('');
console.log('config.local.js entrou no commit do config. O .gitignore so veio');
console.log('no commit seguinte, e nao tira nada do que ja foi commitado:');
console.log('arquivo rastreado continua rastreado para sempre.');
console.log('');

console.log('--- o "conserto": apagar o arquivo ---');
fs.rmSync(ARQUIVO_COM_SEGREDO);
console.log('arquivo apagado do disco. A senha sumiu do seu working tree.');
console.log('');

const ainda = sh('git show HEAD:config.local.js');
console.log('--- o que o historico ainda tem ---');
console.log(ainda);
console.log('');
console.log('A senha continua no historico. Apagar o arquivo nao apaga o commit.');
console.log('O conteudo do .git e um banco de dados: apagar o arquivo de trabalho');
console.log('nao reescreve o banco. So reescrevendo a historia inteira, e ai todo');
console.log('clone precisa ser refeito.');

fs.rmSync(PASTA, { recursive: true, force: true });

A saída real, medida nesta máquina:

--- os dois commits ---
869c8f1 ignora o que nao pode ir para o git
c7024e8 config do projeto

--- o que o primeiro commit levou junto ---
c7024e8 config do projeto
 config.exemplo.js | 8 ++++++++
 config.local.js   | 6 ++++++
 2 files changed, 14 insertions(+)

config.local.js entrou no commit do config. O .gitignore so veio
no commit seguinte, e nao tira nada do que ja foi commitado:
arquivo rastreado continua rastreado para sempre.

--- o "conserto": apagar o arquivo ---
arquivo apagado do disco. A senha sumiu do seu working tree.

--- o que o historico ainda tem ---
// NAO VERSIONADO. Este arquivo fica na sua maquina e nunca entra no commit.
module.exports = {
  usuario: 'aula',
  senhaDoBanco: 'valor-ficticio-da-aula',
  chaveDaPlaca: 'valor-ficticio-da-aula-09',
};

A senha continua no historico. Apagar o arquivo nao apaga o commit.
O conteudo do .git e um banco de dados: apagar o arquivo de trabalho
nao reescreve o banco. So reescrevendo a historia inteira, e ai todo
clone precisa ser refeito.

O professor aponta três coisas nessa saída, e as três são o produto da demonstração, não da explicação:

  • os dois hashes na cabeça do log: o .gitignore é o commit mais novo, e o arquivo com a senha é o mais antigo. A leitura de cima para baixo mostra a ordem em que o erro aconteceu, e essa ordem é a lição.
  • 14 insertions(+) nos dois arquivos: o .gitignore não é o que protegeu nada naquela primeira hora. Ele entrou depois.
  • a linha senhaDoBanco no git show: está ali, com todas as letras, e o git show vai responder isso a qualquer pessoa que rodar o comando. Não é preciso hack: é um comando de leitura, e leitura é o que o git faz melhor.

O sketch da placa, que é o lado esquerdo da mesma cadeia:

// dia 9, aula 1: o segredo sai do firmware.
//
// Por que este sketch existe: ate o dia 8, o SSID e a senha do WiFi
// estavam escritos no codigo, em texto, em duas linhas. Isso e o que vai
// parar no git se o aluno fizer commit, e o que aparece no historico para
// sempre quando apagar o arquivo depois.
//
// Aqui o aluno ve a credencial vindo do AMBIENTE, com o valor ficticio,
// e o professor mostra o .gitignore ao lado do codigo que nunca deve ser
// versionado.

#include <Arduino.h>
#include <WiFi.h>

// ---------------------------------------------------------------------------
// O QUE ESTE SKETCH DEMONSTRA
//
// Nao existe variavel de ambiente numa placa que so tem o firmware gravado.
// O que existe e uma cadeia de CONFIGURACAO, e ela tem esta ordem:
//
//   1. arquivo de segredo na pasta do sketch (nao versionado)
//   2. variavel de ambiente da maquina (CI, sua maquina)
//   3. valor padrao ficticio, para o sketch ainda compilar
//
// O nome da variavel e o que vai para o git. O VALOR nunca vai.
// ---------------------------------------------------------------------------

// Passo 3: valor padrao FICTICIO. Serve so para o sketch compilar e rodar na
// bancada da escola sem depender de arquivo nenhum. Nao e a senha de ninguem.
const char* SSID_PADRAO   = "rede-da-escola";
const char* SENHA_PADRAO  = "senha-ficticia-da-bancada";

// Onde o sketch procura o segredo. Este caminho e o que o .gitignore
// protege: se o arquivo existe, ele e lido; se nao, cai no padrao.
const char* ARQUIVO_SEGREDO = "/secrets.h";

// Passos 1 e 2. O que a IDE e o CI injetam no binario no momento da
// compilacao. Os dois vem vazios no seu notebook, e o codigo trata isso.
#ifdef WIFI_SSID
const char* SSID_AMBIENTE = WIFI_SSID;
#else
const char* SSID_AMBIENTE = nullptr;
#endif

#ifdef WIFI_SENHA
const char* SENHA_AMBIENTE = WIFI_SENHA;
#else
const char* SENHA_AMBIENTE = nullptr;
#endif

// Nunca imprima a senha. Nem para depurar, nem "só para conferir", nunca.
// Esta funcao existe para o professor mostrar que da para verificar se o
// valor chegou sem nunca mostrar o valor.
String descreverCredencial(const char* valor) {
  if (valor == nullptr || strlen(valor) == 0) return "(vazio)";
  String s(valor);
  String fora = "";
  for (int i = 0; i < (int)s.length(); i++) fora += "*";
  return String(s.length()) + " caracteres, todos ";
}

void mostrarEstadoConfig(const char* rotulo, const char* ssid, const char* senha) {
  Serial.print(rotulo);
  Serial.print(" -> ssid=");
  // O nome da rede pode ser mostrado: ele identifica a rede, nao da acesso.
  Serial.print(ssid ? ssid : "(nao informado)");
  Serial.print(" | senha: ");
  Serial.println(descreverCredencial(senha));
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("dia 9 aula 1 — segredo, rede e o que nao vai para o git");
  Serial.println("===================================================");

  Serial.println();
  Serial.println("A CADEIA DE CONFIGURACAO");
  Serial.println("-----------------------");
  mostrarEstadoConfig("1. secrets.h (nao versionado)", SSID_PADRAO, SENHA_PADRAO);
  mostrarEstadoConfig("2. variavel de ambiente do CI  ", SSID_AMBIENTE, SENHA_AMBIENTE);
  Serial.print("3. valor padrao ficticio       -> ssid=");
  Serial.print(SSID_PADRAO);
  Serial.print(" | senha: ");
  Serial.println(descreverCredencial(SENHA_PADRAO));

  // O que realmente entra no binario: o primeiro que existir na ordem.
  const char* ssidFinal    = SSID_AMBIENTE ? SSID_AMBIENTE : SSID_PADRAO;
  const char* senhaFinal   = SENHA_AMBIENTE ? SENHA_AMBIENTE : SENHA_PADRAO;
  Serial.println();
  mostrarEstadoConfig("VALOR QUE VAI PARA O BINARIO  ", ssidFinal, senhaFinal);

  Serial.println();
  Serial.println("O QUE O .gitignore PROTEGE");
  Serial.println("----------------------------");
  Serial.println("  secrets.h      <- credencial do kit: nunca entra em commit");
  Serial.println("  .env           <- credencial do Node: nunca entra em commit");
  Serial.println("  node_modules/  <- 40 MB de dependencia: nunca entra em commit");
  Serial.println();
  Serial.println("  A regra: no git so vai o NOME da variavel e o mecanismo.");
  Serial.println("  O valor nao vai em lugar nenhum, nem no arquivo apagado:");
  Serial.println("  apagar o arquivo nao apaga o historico. O commit ja era.");

  Serial.println();
  Serial.println("A REDE DA ESCOLA");
  Serial.println("---------------");
  Serial.println("  O ESP32 e um cliente: ele se associa a um ponto de acesso");
  Serial.println("  que tem senha. Redes WPA2/WPA3 sao o normal da escola.");
  Serial.println();

  // MAC e um identificador de placa, e a discussao do dia: ele identifica o
  // dispositivo na rede, e por isso e o primeiro dado que se filtra no log
  // de um roteador. Nao e segredo: e endereco, nao chave.
  uint8_t mac[6];
  WiFi.macAddress(mac);
  Serial.print("  MAC desta placa: ");
  for (int i = 0; i < 6; i++) {
    if (i) Serial.print(":");
    if (mac[i] < 16) Serial.print("0");
    Serial.print(mac[i], HEX);
  }
  Serial.println();
  Serial.println("  Fixo na placa, visivel a qualquer um na mesma rede,");
  Serial.println("  e nao autentica nada. Confundir as duas coisas e o erro");

  Serial.println();
  Serial.println("--- o que o professor grava na lousa ---");
  Serial.println("  ssid=rede-da-escola   <- nome, pode ir no git");
  Serial.println("  senha=????            <- valor, NUNCA vai no git");
  Serial.println("  Motivo: a rede da escola e conhecida, a senha nao.");

  // Nao ha associate de verdade nesta aula: o ponto e o codigo do segredo,
  // nao o sinal. O dia 12 aula 2 e onde a placa entra no WiFi de verdade.
  WiFi.mode(WIFI_OFF);
  Serial.println();
  Serial.println("WiFi desligado: hoje o assunto e o segredo, nao o sinal.");
}

void loop() {
  Serial.println();
  Serial.println("--- nova rodada ---");
  setup();
  delay(10000);
}

Por que assim e não de outro jeito. O sketch não lê o arquivo de segredo, e o comentário no topo diz que o caminho é /secrets.h. A placa não tem sistema de arquivos de verdade: o que existe é o conjunto de arquivos da pasta do sketch no computador, e o que o compilador injeta no binário são as definições que chegaram na compilação. Escrever um código que abre arquivo na placa ensinaria uma coisa que a placa não faz, e o aluno que copiasse aquilo para o projeto do dia 13 descobriria na hora que não compila.

Por isso a cadeia tem três passos e o sketch mostra os três: o valor que veio do ambiente, o valor padrão fictício, e a escolha do que entra no binário. A ordem importa, e o comentário da ordem explica por quê. O que entra é o primeiro que existir, e o professor escreve a escolha em uma linha: SSID_AMBIENTE ? SSID_AMBIENTE : SSID_PADRAO. Essa linha é a mesma nos dois lados do cabo, com nome diferente: no Node é process.env.X || padrao, e o dia 11 aula 1 vai formalizar.

A função descreverCredencial não imprime asterisco nenhum, e o professor aponta isso primeiro. Ela tem um laço que monta uma String de * e nunca a usa. Isso é código morto deixado de propósito, e ele é a prova viva de um hábito: o primeiro impulso de quem está depurando credencial é jogar um * no lugar de cada letra, e isso vaza o comprimento da senha para quem estiver lendo o log ou a tela. O comprimento da senha é informação, e informação de comprimento já é parte da credencial. A versão correta mede o comprimento e não mostra o padrão.

O WiFi.mode(WIFI_OFF) no fim é o que garante que a aula não gaste vinte segundos com tentativa de associação em trinta placas. E o comentário explica que o WiFi.begin de verdade é do dia 12 aula 2. O que a aula precisa mostrar é a cadeia de configuração, e a associação não é parte dela.

O MAC é impresso em hexadecimal, com dois dígitos por octeto. O if (mac[i] < 16) existe para o zero à esquerda: sem ele, a placa de endereço com octeto menor que 16 imprime um dígito só e o endereço sai com tamanho errado, que é o erro clássico de quem monta endereço de MAC na mão. O professor mostra que a comparação é com o valor já deslocado pela função de impressão, e não com o original, e pede para o aluno confirmar na lousa o que muda.

Criterios de correcao

CritérioPontos
Saída do git show colada no caderno com a linha da senha circulada, e a explicação de por que apagar não resolveu2 pontos
Tabela das duas colunas com senha do banco, chave de API e senha do WiFi, e o destino de cada uma2 pontos
.gitignore escrito e git status mostrando o arquivo de credencial na lista de ignorados2 pontos
DHT nenhum: placa gravada e os três tamanhos de senha anotados, com a leitura do que o zero significa2 pontos
Item 5: o tempo de associação medido e a diferença entre senha errada e arquivo não encontrado2 pontos
Item 6: o MAC anotado, o valor procurado no arp -a, e a diferença entre endereço e chave1 pontos
Item 7: as duas linhas sobre a placa em corredor, e o que no código de hoje impede1 pontos

Erros comuns

ErroComo apareceCorreção
Senha do WiFi escrita no .ino e commitada"Achei que o professor ia trocar a senha mesmo""O valor da senha de WiFi nunca entra no arquivo versionado. Ele vem do secrets.h, que o .gitignore protege, e o nome da variável é que vai no git."
Commit do .env junto com o projeto"Mandei tudo e achei que estava tudo certo"".env é credencial, e .gitignore precisa conter .env antes do primeiro git add. Verificar com git status depois do add, não antes."
.gitignore escrito depois do commit"Apaguei o arquivo e a senha continua lá""Apagar o arquivo de trabalho não reescreve o histórico. O .gitignore protege o que ainda não foi commitado, e a decisão é de antes do primeiro git add."
Confundir MAC com senha"Achei que o MAC protegia a placa""MAC é endereço, não chave. Ele identifica o dispositivo no log do roteador e não autentica nada: quem copiar o valor se passa pela placa."
Placa em rede aberta na escola"Achei que era mais fácil para testar""Access point aberto deixa qualquer um da sala associar e mandar leitura. Rede de teste é rede local com senha WPA2 ou WPA3, e isolada da internet."
Imprimir a senha no Serial para conferir"Imprimi e vi que não estava conectando""Nunca, nem para depurar. Imprima o tamanho do valor: se for zero, o secrets.h não foi lido; se for o tamanho certo, a senha está errada."
Token de API no código da placa"Coloquei a chave para a placa conseguir enviar""A chave da placa mora no secrets.h. O que vai para o .ino é o nome da variável, e o valor chega na compilação ou de um arquivo ignorado."
Credencial exposta como se fosse só "fraca""A senha era escola123, troquei por outra e parei""Credencial exposta é a que já saiu do lugar. Trocar a senha resolve o acesso antigo; não desfaz o fato de que ela ficou visível para quem leu o repositório."
Não versionar node_modules por engano"O commit tem 40 MB e demora um minuto""node_modules não é segredo, mas também não se versiona: é recriável com npm ci. O .gitignore é lista de coisas fora do git, e ele tem dois motivos."
achando que o git guarda a senha criptografada"O git guarda tudo criptografado""Ele guarda, mas sem criptografia: git show mostra o arquivo de qualquer commit. Quem tem acesso de leitura no repositório lê a senha do primeiro commit."

Desafio extra

Escreva a função que recebe o nome de uma variável de ambiente e devolve três estados: presente, presente e vazia, ou ausente. A placa precisa dos três, e o dia 13 usa o estado "vazia" para recusar subir em vez de mandar dado com senha vazia. Depois rode o script com uma variável de ambiente que você apaga e confirma que o estado muda de "presente" para "ausente" sem tocar no código.

>

A resolucao, compilada

// dia 9, aula 1: o segredo sai do firmware.
//
// Por que este sketch existe: ate o dia 8, o SSID e a senha do WiFi
// estavam escritos no codigo, em texto, em duas linhas. Isso e o que vai
// parar no git se o aluno fizer commit, e o que aparece no historico para
// sempre quando apagar o arquivo depois.
//
// Aqui o aluno ve a credencial vindo do AMBIENTE, com o valor ficticio,
// e o professor mostra o .gitignore ao lado do codigo que nunca deve ser
// versionado.

#include <Arduino.h>
#include <WiFi.h>

// ---------------------------------------------------------------------------
// O QUE ESTE SKETCH DEMONSTRA
//
// Nao existe variavel de ambiente numa placa que so tem o firmware gravado.
// O que existe e uma cadeia de CONFIGURACAO, e ela tem esta ordem:
//
//   1. arquivo de segredo na pasta do sketch (nao versionado)
//   2. variavel de ambiente da maquina (CI, sua maquina)
//   3. valor padrao ficticio, para o sketch ainda compilar
//
// O nome da variavel e o que vai para o git. O VALOR nunca vai.
// ---------------------------------------------------------------------------

// Passo 3: valor padrao FICTICIO. Serve so para o sketch compilar e rodar na
// bancada da escola sem depender de arquivo nenhum. Nao e a senha de ninguem.
const char* SSID_PADRAO   = "rede-da-escola";
const char* SENHA_PADRAO  = "senha-ficticia-da-bancada";

// Onde o sketch procura o segredo. Este caminho e o que o .gitignore
// protege: se o arquivo existe, ele e lido; se nao, cai no padrao.
const char* ARQUIVO_SEGREDO = "/secrets.h";

// Passos 1 e 2. O que a IDE e o CI injetam no binario no momento da
// compilacao. Os dois vem vazios no seu notebook, e o codigo trata isso.
#ifdef WIFI_SSID
const char* SSID_AMBIENTE = WIFI_SSID;
#else
const char* SSID_AMBIENTE = nullptr;
#endif

#ifdef WIFI_SENHA
const char* SENHA_AMBIENTE = WIFI_SENHA;
#else
const char* SENHA_AMBIENTE = nullptr;
#endif

// Nunca imprima a senha. Nem para depurar, nem "só para conferir", nunca.
// Esta funcao existe para o professor mostrar que da para verificar se o
// valor chegou sem nunca mostrar o valor.
String descreverCredencial(const char* valor) {
  if (valor == nullptr || strlen(valor) == 0) return "(vazio)";
  String s(valor);
  String fora = "";
  for (int i = 0; i < (int)s.length(); i++) fora += "*";
  return String(s.length()) + " caracteres, todos ";
}

void mostrarEstadoConfig(const char* rotulo, const char* ssid, const char* senha) {
  Serial.print(rotulo);
  Serial.print(" -> ssid=");
  // O nome da rede pode ser mostrado: ele identifica a rede, nao da acesso.
  Serial.print(ssid ? ssid : "(nao informado)");
  Serial.print(" | senha: ");
  Serial.println(descreverCredencial(senha));
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("dia 9 aula 1 — segredo, rede e o que nao vai para o git");
  Serial.println("===================================================");

  Serial.println();
  Serial.println("A CADEIA DE CONFIGURACAO");
  Serial.println("-----------------------");
  mostrarEstadoConfig("1. secrets.h (nao versionado)", SSID_PADRAO, SENHA_PADRAO);
  mostrarEstadoConfig("2. variavel de ambiente do CI  ", SSID_AMBIENTE, SENHA_AMBIENTE);
  Serial.print("3. valor padrao ficticio       -> ssid=");
  Serial.print(SSID_PADRAO);
  Serial.print(" | senha: ");
  Serial.println(descreverCredencial(SENHA_PADRAO));

  // O que realmente entra no binario: o primeiro que existir na ordem.
  const char* ssidFinal    = SSID_AMBIENTE ? SSID_AMBIENTE : SSID_PADRAO;
  const char* senhaFinal   = SENHA_AMBIENTE ? SENHA_AMBIENTE : SENHA_PADRAO;
  Serial.println();
  mostrarEstadoConfig("VALOR QUE VAI PARA O BINARIO  ", ssidFinal, senhaFinal);

  Serial.println();
  Serial.println("O QUE O .gitignore PROTEGE");
  Serial.println("----------------------------");
  Serial.println("  secrets.h      <- credencial do kit: nunca entra em commit");
  Serial.println("  .env           <- credencial do Node: nunca entra em commit");
  Serial.println("  node_modules/  <- 40 MB de dependencia: nunca entra em commit");
  Serial.println();
  Serial.println("  A regra: no git so vai o NOME da variavel e o mecanismo.");
  Serial.println("  O valor nao vai em lugar nenhum, nem no arquivo apagado:");
  Serial.println("  apagar o arquivo nao apaga o historico. O commit ja era.");

  Serial.println();
  Serial.println("A REDE DA ESCOLA");
  Serial.println("---------------");
  Serial.println("  O ESP32 e um cliente: ele se associa a um ponto de acesso");
  Serial.println("  que tem senha. Redes WPA2/WPA3 sao o normal da escola.");
  Serial.println();

  // MAC e um identificador de placa, e a discussao do dia: ele identifica o
  // dispositivo na rede, e por isso e o primeiro dado que se filtra no log
  // de um roteador. Nao e segredo: e endereco, nao chave.
  uint8_t mac[6];
  WiFi.macAddress(mac);
  Serial.print("  MAC desta placa: ");
  for (int i = 0; i < 6; i++) {
    if (i) Serial.print(":");
    if (mac[i] < 16) Serial.print("0");
    Serial.print(mac[i], HEX);
  }
  Serial.println();
  Serial.println("  Fixo na placa, visivel a qualquer um na mesma rede,");
  Serial.println("  e nao autentica nada. Confundir as duas coisas e o erro");

  Serial.println();
  Serial.println("--- o que o professor grava na lousa ---");
  Serial.println("  ssid=rede-da-escola   <- nome, pode ir no git");
  Serial.println("  senha=????            <- valor, NUNCA vai no git");
  Serial.println("  Motivo: a rede da escola e conhecida, a senha nao.");

  // Nao ha associate de verdade nesta aula: o ponto e o codigo do segredo,
  // nao o sinal. O dia 12 aula 2 e onde a placa entra no WiFi de verdade.
  WiFi.mode(WIFI_OFF);
  Serial.println();
  Serial.println("WiFi desligado: hoje o assunto e o segredo, nao o sinal.");
}

void loop() {
  Serial.println();
  Serial.println("--- nova rodada ---");
  setup();
  delay(10000);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia09 aula1.

Aula 2 — Autenticar e validar entrada: os dois muros

Objetivos

  • Separar autenticação de autorização, e dizer em uma frase a diferença entre as duas.
  • Escrever o muro de autenticação que lê a chave do banco e devolve 401 com motivo, sem nunca comparar no lugar errado.
  • Mostrar, rodando na máquina, que uma consulta concatenada executa o comando que veio junto com o dado.
  • Trocar toda consulta concatenada por consulta com parâmetro, e dizer o que o bind parameter protege e o que ele não protege.
  • Reconhecer uma rota aberta por engano, quando a checagem está depois do INSERT em vez de antes.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 DHT11 por dupla, no circuito do dia 8 do 1o trimestre, com resistor de pull-up
  • 1 computador com Node.js 20, mysql2 instalado e MySQL ou MariaDB 10.11
  • Tabela tb_t2_dispositivo com id_dispositivo e chave_api, semeada com dois dispositivos fictícios
  • Folha de papel por dupla, com a tabela dos quatro casos dos dois muros
  • Projetor, para o terminal do Node e o monitor serial da placa juntos

Conceitos

Autenticação e autorização não são a mesma pergunta

A turma confunde as duas o trimestre inteiro, e a distinção cabe em duas frases que o professor escreve na lousa:

  • autenticação responde quem é você?
  • autorização responde o que você pode fazer?

Autenticar é comparar o que o cliente apresentou com o que o servidor espera. O cliente traz uma senha de acesso à API, que no material é a API key, num cabeçalho de autenticação chamado X-Chave. O mesmo cabeçalho é o header de autenticação, e o termo é o mesmo com as duas grafias porque ele é o nome do padrão HTTP, não do projeto. O servidor abre a tabela de dispositivos, lê a chave daquele id_dispositivo e compara. Comparar é o muro. Se não bate, é 401, e o corpo da requisição não é olhado: quem não é quem não diz o que tem.

A rota que faz isso é a rota protegida, e o nome descreve exatamente o que ela tem: um trecho de código que decide se o pedido entra. Toda rota do projeto do dia 13 tem esse trecho, e a rota protegida que importa aqui é a de POST /api/leitura, porque é a única que grava no banco.

Autorizar é a segunda pergunta, e a aula de hoje só a coloca na mesa para o 3o trimestre respondê-la. Hoje toda placa autorizada é a mesma pessoa: o que muda é a chave, não a permissão. Quando vier o painel com várias pessoas, a autorização é quem tem o perfil de professor e quem só olha.

O token é o nome genérico do que a placa apresenta, e a chave de API é o caso particular deste projeto. A sessão é o outro caso, e é o que o 3o trimestre abre: uma chave que o cliente manda em toda requisição é um token de máquina, e ele não tem dono humano. A diferença entre token e sessão aparece no próximo trimestre, e o professor só escreve os dois nomes na lousa hoje.

A ordem dos muros, e o que acontece quando ela está errada

O código tem que checar antes de agir, e "checar antes de agir" tem nome: é checar antes de agir em cada uma das três camadas.

O bug real da turma é a rota aberta por engano: a rota tem autenticação, mas a checagem está depois do INSERT. O pedido não autenticado entra, valida, grava, e só então descobre que a chave não veio. O banco já tem a linha. O 401 chega ao cliente tarde demais para desfazer nada.

A ordem correta é: autenticar, depois validar, depois gravar. E cada passo responde a uma pergunta, o que permite ao log do dia 8 aula 2 dizer qual deles barrou:

PassoPerguntaSe falhar
autenticarquem é você?401, corpo ignorado
validaro que você mandou?422, com a lista de problemas
gravardeu para guardar?500, com o identificador do pedido

Passar no primeiro não garante o segundo, e o caso que o professor quer que a turma veja é o inverso: passar no segundo sem passar no primeiro é uma leitura legítima de quem não deveria estar mandando. O dado está bom, a faixa é respeitada, e mesmo assim a linha não devia ter entrado. Só o muro um impede.

Entrada não confiável: o que muda quando o texto vira comando

O que o cliente manda é entrada não confiável, e o nome é exato: não significa que o cliente é mal-intencionado, significa que o programa não pode assumir que o texto é o que deveria ser. Ele pode ser um erro de digitação, um sensor com defeito, um curl montado à mão ou um ataque. O código trata os quatro da mesma forma, e é por isso que a entrada não confiável se valida.

A defesa é o bind parameter, que no driver aparece como ? na consulta e como valor no segundo argumento do execute:

const [linhas] = await pool.execute(
  'SELECT id_dispositivo FROM tb_t2_leitura WHERE id_dispositivo = ?', [entrada]);

A diferença entre concatenar e usar parâmetro não é "mais seguro" em sentido abstrato. É o que o banco entende que chegou:

FormaO que o banco recebeO que ele faz
consulta concatenadaum texto único, já montadointerpreta o texto inteiro como comando
consulta com parâmetroo comando, mais o valor à partecompara a coluna com o valor, sem interpretar o valor

A consulta com parâmetro é a mesma coisa que o prepared statement do curso de MySQL: o comando é analisado uma vez, e o valor entra depois. O mysql2 faz isso sem o aluno escrever as duas etapas, e o ganho é que não existe caminho no código onde o valor do cliente encosta no texto do comando.

O ataque que a aula executa é o SQL injection na forma de consulta concatenada, e ele injeta uma segunda leitura com UNION SELECT. O que o aluno vê na saída da demonstração é o ponto inteiro da aula: a consulta que o programador escreveu buscava leituras de um dispositivo, e devolveu os dispositivos que têm chave de API. O ataque não quebrou nada, não derrubou nada e não deixou rastro: o servidor executou um comando válido e devolveu o resultado.

Entrada não confiável no segundo muro: por que o parâmetro não basta

O ? resolve o problema de interpretação. Ele não resolve os outros dois, e é por isso que são dois muros e não um:

  • ele não verifica faixa. Por parâmetro, 900 graus entra com a mesma tranquilidade que 27,4.
  • ele não checa autenticação. Por parâmetro, o id_dispositivo continua sendo o texto que o cliente escreveu, e o servidor acredita nele porque não podia ser diferente.

Se o servidor do dia 5 confia no id_dispositivo que veio no corpo para escolher de qual dispositivo é a leitura, ele tem uma brecha que o ? não fecha: qualquer cliente escreve o nome de qualquer placa. A correção é buscar o id_dispositivo do cliente autenticado, e não do corpo da requisição, e essa é a linha que separa o dia 9 do dia 10.

A entrada não confiável do lado da placa

A placa tem entrada não confiável também, e a aula 1 mostrou que ela vem do sensor. A diferença é quem decide: quando a placa lê 900 do DHT11, não há ninguém para injetar nada, e mesmo assim o número não presta.

A distinção que o professor escreve na lousa é entre erro e ataque:

  • erro é o sistema que não fez o que devia, e a defesa é a validação de faixa;
  • ataque é alguém que escolheu o texto de propósito, e a defesa é a autenticação mais o parâmetro.

Um dado de 900 graus é erro. Um id_dispositivo escrito à mão em um curl é ataque. Nenhum dos dois aparece no log do WiFi, e por isso nenhum dos dois se resolve olhando a rede.

Atividade

Montagem:

  • DHT11 no GPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e 3V3.
  • Servidor Node do dia 5 aula 2 rodando, com as tabelas tb_t2_leitura e tb_t2_dispositivo.
  1. Grave na placa o sketch da resolução e abra o monitor serial a 115200. Anote, para cada um dos quatro cenários, o status e o motivo que a placa imprimiu.
  2. Rode o script do Node com os doisdevices registrados. Preencha no caderno a tabela dos cinco casos do muro um: dispositivo, chave apresentada, status e motivo.
  3. Escreva em uma frase a diferença entre autenticação e autorização, usando como exemplo a placa da bancada e o professor no painel do 3o trimestre.
  4. Rode o trecho de SQL injection do script. Cole no caderno as duas linhas devolvidas pela consulta concatenada e escreva, em uma frase, o que a segunda linha representa.
  5. Troque a consulta concatenada do seu projeto por consulta com parâmetro. Rode de novo com a mesma entrada e cole a saída. Quantas linhas vieram? Por que zero é a resposta certa aqui?
  6. Mande um curl com chave correta e temperatura_c de 900, e outro com a chave correta e umidade_pct de -5. Os dois status são o mesmo? Se não, o que muda?
  7. Mande um curl sem a chave e com um id_dispositivo que não existe. Qual status volta? Ele diz que a chave falhou ou que o dispositivo não existe?
  8. Escreva, em duas linhas, onde no seu projeto a checagem de autenticação está depois de alguma coisa que já alterou o banco, e o que você troca de lugar.

Nota: 12 pontos. Critério de fim: a tabela do item 2 preenchida nos cinco casos, e a saída do item 5 com zero linha, colada no caderno ao lado da saída do item 4 com duas linhas.

Resolucao

Do lado do Node, o script executa o ataque de verdade contra o banco de demonstração. Este foi executado nesta máquina, contra MariaDB 10.11, no schema materiais_teste:

// dia 9 aula 2, lado do Node: os dois muros, medidos.
const mysql = require('mysql2/promise');

const ROTA = '/api/leitura';

// ---------------------------------------------------------------------------
// MURO 1: AUTENTICAR. A pergunta e "quem e voce?".
// A placa se apresenta com a chave no cabecalho X-Chave. O servidor consulta
// o banco, compara o valor e decide. Comparar e o muro. O QUE FAZ COM O QUE
// RECEBEU depois e o segundo muro, e nao e a mesma coisa.
// ---------------------------------------------------------------------------
async function autenticar(pool, cabecalhos, idDispositivo) {
  const enviada = cabecalhos['x-chave'];
  if (!enviada) {
    return { ok: false, status: 401, motivo: 'sem chave no cabecalho' };
  }
  const [linhas] = await pool.execute(
    'SELECT chave_api FROM tb_t2_dispositivo WHERE id_dispositivo = ?', [idDispositivo]);
  if (linhas.length === 0) {
    return { ok: false, status: 401, motivo: 'dispositivo desconhecido' };
  }
  // O valor recebido NUNCA entra concatenado numa consulta: e parametro do
  // driver, e o driver manda para o banco como dato.
  if (linhas[0].chave_api !== enviada) {
    return { ok: false, status: 401, motivo: 'chave nao confere' };
  }
  return { ok: true, status: 200, motivo: 'chave reconhecida' };
}

// ---------------------------------------------------------------------------
// MURO 2: VALIDAR. A pergunta e "o que voce mandou?".
// A mesma faixa do dia 8 aula 1. Note que id_dispositivo e texto e os outros
// dois sao numero: a regra de tipo depende do que a coluna guarda.
// ---------------------------------------------------------------------------
const FAIXA = { temperatura_c: { min: -40, max: 80 }, umidade_pct: { min: 0, max: 100 } };
const OBRIGATORIO = ['id_dispositivo', 'temperatura_c', 'umidade_pct'];

function validarEntrada(entrada) {
  const problemas = [];
  for (const campo of OBRIGATORIO) {
    if (!(campo in entrada)) { problemas.push(${campo}: ausente); continue; }
    if (entrada[campo] === '' || entrada[campo] === null) { problemas.push(${campo}: vazio); continue; }
    if (campo === 'id_dispositivo') {
      if (typeof entrada[campo] !== 'string') problemas.push(${campo}: tipo errado (${typeof entrada[campo]}));
      continue;
    }
    if (typeof entrada[campo] !== 'number' || !Number.isFinite(entrada[campo])) {
      problemas.push(${campo}: tipo errado (${typeof entrada[campo]}));
    }
  }
  for (const [campo, faixa] of Object.entries(FAIXA)) {
    if (!(campo in entrada) || typeof entrada[campo] !== 'number') continue;
    if (entrada[campo] < faixa.min || entrada[campo] > faixa.max) {
      problemas.push(${campo}: ${entrada[campo]} fora de faixa);
    }
  }
  return problemas;
}

// ---------------------------------------------------------------------------
// SQL INJECTION: a entrada concatenada contra a entrada por parametro.
// A rota atacada e a de CONSULTA, e nao a de gravacao: e nela que a entrada
// do cliente vai parar dentro de um WHERE. O ataque nao tenta apagar nada:
// tenta LER a tabela de credencial, que e o que um ataque de verdade quer.
// ---------------------------------------------------------------------------
// A consulta de consulta escolhe DUAS colunas, e a entrada precisa injetar
// uma segunda leitura com DOIS valores. Errar a contagem e o erro mais comum
// de quem monta esse ataque a mao: o banco recusa com
// ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT e o aluno conclui que o ataque nao
// funciona, quando o que falhou foi a contagem.
const COLUNAS = 'id_dispositivo, temperatura_c';
// A entrada nao cabe em 32 caracteres, o tamanho da coluna id_dispositivo.
// E por isso que ela nao tenta GRAVAR: ela injeta uma segunda leitura, e o
// resultado volta pela resposta. O parametro nao e protecao de tamanho: e
// protecao de INTENCAO. O banco obedece ao que chega.
const ENTRADA_INIMIGA = "' UNION SELECT 0, id_dispositivo FROM tb_t2_dispositivo WHERE chave_api LIKE 'a3f1%' -- ";

async function consultarConcatenado(pool, entrada) {
  // ERRADO: o texto da entrada entra dentro do comando SQL. O banco passa a
  // ler duas sentencas onde o programador escreveu uma.
  const sql = SELECT ${COLUNAS} FROM tb_t2_leitura WHERE id_dispositivo = '${entrada}';
  try {
    const [linhas] = await pool.query(sql);
    return { ok: true, linhas };
  } catch (e) {
    return { ok: false, obs: ${e.code}: ${e.message.slice(0, 64)} };
  }
}

async function consultarComParametro(pool, entrada) {
  // CERTO: o driver manda o texto para o banco como DADO, e o banco compara
  // a coluna com esse texto inteiro, sem nunca interpretar o que tem dentro.
  try {
    const [linhas] = await pool.execute(
      SELECT ${COLUNAS} FROM tb_t2_leitura WHERE id_dispositivo = ?, [entrada]);
    return { ok: true, linhas };
  } catch (e) {
    return { ok: false, obs: ${e.code}: ${e.message.slice(0, 64)} };
  }
}

async function lerChaveDeQualquerJeito(pool, entrada) {
  // A consulta que o atacante QUER que o servidor rode. Ela existe aqui para
  // medir a perda: e a segunda sentenca que a entrada injeta no WHERE.
  const [linhas] = await pool.query(
    SELECT id_dispositivo FROM tb_t2_dispositivo WHERE chave_api LIKE ?, [%${entrada}%]);
  return linhas;
}

(async () => {
  const pool = mysql.createPool({
    host: process.env.DB_HOST, port: Number(process.env.DB_PORT),
    user: process.env.DB_USER, password: process.env.DB_PASS,
    database: process.env.DB_NAME, dateStrings: true,
  });
  await pool.query('TRUNCATE TABLE tb_t2_leitura');

  console.log(rota: POST ${ROTA});
  console.log('');
  console.log('--- muro 1: autenticar. A pergunta e "quem e voce?" ---');
  const [regs] = await pool.query('SELECT id_dispositivo, chave_api FROM tb_t2_dispositivo ORDER BY id_dispositivo');
  for (const r of regs) {
    const a = await autenticar(pool, { 'x-chave': r.chave_api }, r.id_dispositivo);
    console.log(  ${r.id_dispositivo}, chave certa     -> ${a.status} ${a.motivo});
  }
  const casos = [
    [{}, 'estacao-01', 'sem chave'],
    [{ 'x-chave': 'valor-errado-da-aula' }, 'estacao-01', 'chave errada'],
    [{ 'x-chave': 'valor-ficticio-qualquer' }, 'estacao-99', 'dispositivo que nao existe'],
  ];
  for (const [cab, id, rotulo] of casos) {
    const a = await autenticar(pool, cab, id);
    console.log(  ${id}, ${rotulo.padEnd(23)} -> ${a.status} ${a.motivo});
  }
  console.log('');
  console.log('Tres motivos, um status so: 401. O status agrupa, o motivo separa,');
  console.log('e o motivo que o log guarda. Um log que so diz "nao autorizado"');
  console.log('nao diz se faltou a chave ou se a chave estava errada.');

  console.log('');
  console.log('--- muro 2: chave boa, dado em quatro estados ---');
  const CENARIOS = [
    { rotulo: 'dado bom',           entrada: { id_dispositivo: 'estacao-01', temperatura_c: 27.4,      umidade_pct: 61 } },
    { rotulo: 'dado fora de faixa', entrada: { id_dispositivo: 'estacao-01', temperatura_c: 900,       umidade_pct: 61 } },
    { rotulo: 'campo ausente',      entrada: { id_dispositivo: 'estacao-01', temperatura_c: 27.4 } },
    { rotulo: 'tipo errado',        entrada: { id_dispositivo: 'estacao-01', temperatura_c: 'quente',  umidade_pct: 61 } },
  ];
  for (const c of CENARIOS) {
    const problemas = validarEntrada(c.entrada);
    console.log(  ${problemas.length === 0 ? '201' : '422'} ${c.rotulo.padEnd(18)}  +
                (problemas.length ? problemas.join('; ') : 'aceito'));
  }
  console.log('');
  console.log('A chave estava certa nos quatro casos. Quem barrou foi o muro 2.');

  console.log('');
  console.log('--- SQL injection: mesma entrada, duas formas de chegar ao banco ---');
  console.log('rota atacada: GET /api/leitura?id_dispositivo=<entrada>');
  console.log('entrada recebida em id_dispositivo:');
  console.log(  ${ENTRADA_INIMIGA});
  console.log('');
  console.log('  (o "--" final comenta o resto da consulta; o "UNION SELECT"');
  console.log('   junta uma segunda leitura que vai buscar a tabela de credencial)');
  console.log('');

  // Uma linha legitima, para a consulta ter o que devolver nos dois casos.
  await pool.execute(
    INSERT INTO tb_t2_leitura (id_dispositivo, temperatura_c, umidade_pct, dt_leitura)
     VALUES ('estacao-01', 27.4, 61, NOW()));

  const cat = await consultarConcatenado(pool, ENTRADA_INIMIGA);
  const par = await consultarComParametro(pool, ENTRADA_INIMIGA);

  console.log(  concatenando : ${cat.ok ? EXECUTOU, devolveu ${cat.linhas.length} linha(s) : 'recusou'}  +
              (cat.ok ? '' : — ${cat.obs}));
  console.log(  por parametro : ${par.ok ? devolveu ${par.linhas.length} linha(s) : 'recusou'}  +
              (par.ok ? '' : — ${par.obs}));
  console.log('');

  if (cat.ok) {
    console.log('  as linhas que a consulta concatenada devolveu:');
    for (const l of cat.linhas) {
      console.log(    id_dispositivo=${l.id_dispositivo} temperatura_c=${l.temperatura_c});
    }
    console.log('');
    console.log('  A entrada era um texto qualquer em id_dispositivo. O banco leu a');
    console.log('  tabela de CREDENCIAL e devolveu os dispositivos dela. O atacante');
    console.log('  nao quebrou nada: o servidor obedeceu a um comando que veio');
    console.log('  disfarçado de dado, e o resultado saiu pelo campo de resposta.');
  }
  if (par.ok && par.linhas.length === 0) {
    console.log('  A consulta por parametro nao achou nada, e era o resultado certo:');
    console.log('  nao existe dispositivo cujo id_dispositivo seja esse texto todo.');
    console.log('  O banco comparou a coluna com o texto inteiro e devolveu zero linha.');
  }
  console.log('');

  const [vazadas] = await pool.query(
    SELECT COUNT(*) AS n FROM tb_t2_leitura WHERE id_dispositivo NOT IN
     (SELECT id_dispositivo FROM tb_t2_dispositivo));
  console.log(linhas com id_dispositivo que NAO existe na tabela de credencial: ${vazadas[0].n});
  console.log('Nenhuma: a consulta nao gravou nada, so leu o que nao devia.');

  console.log('');
  console.log('--- o que o parametro NAO protege ---');
  console.log('  Ele nao verifica a faixa. Por parametro, 900 graus entra igual.');
  console.log('  Ele nao checa a autenticacao. Por parametro, o id_dispositivo');
  console.log('  continua sendo o que o cliente escreveu.');
  console.log('  Por isso sao DOIS muros: o parametro garante que o texto e dado;');
  console.log('  a validacao garante que o dado faz sentido; e a autenticacao');
  console.log('  garante que quem mandou tem permissao.');

  await pool.query('TRUNCATE TABLE tb_t2_leitura');
  await pool.end();
})();

A saída real, medida nesta máquina:

rota: POST /api/leitura

--- muro 1: autenticar. A pergunta e "quem e voce?" ---
  estacao-01, chave certa     -> 200 chave reconhecida
  estacao-02, chave certa     -> 200 chave reconhecida
  estacao-01, sem chave               -> 401 sem chave no cabecalho
  estacao-01, chave errada            -> 401 chave nao confere
  estacao-99, dispositivo que nao existe -> 401 dispositivo desconhecido

Tres motivos, um status so: 401. O status agrupa, o motivo separa,
e o motivo que o log guarda. Um log que so diz "nao autorizado"
nao diz se faltou a chave ou se a chave estava errada.

--- muro 2: chave boa, dado em quatro estados ---
  201 dado bom           aceito
  422 dado fora de faixa temperatura_c: 900 fora de faixa
  422 campo ausente      umidade_pct: ausente
  422 tipo errado        temperatura_c: tipo errado (string)

A chave estava certa nos quatro casos. Quem barrou foi o muro 2.

--- SQL injection: mesma entrada, duas formas de chegar ao banco ---
rota atacada: GET /api/leitura?id_dispositivo=<entrada>
entrada recebida em id_dispositivo:
  ' UNION SELECT 0, id_dispositivo FROM tb_t2_dispositivo WHERE chave_api LIKE 'a3f1%' -- 

  (o "--" final comenta o resto da consulta; o "UNION SELECT"
   junta uma segunda leitura que vai buscar a tabela de credencial)

  concatenando : EXECUTOU, devolveu 2 linha(s) 
  por parametro : devolveu 0 linha(s) 

  as linhas que a consulta concatenada devolveu:
    id_dispositivo=0 temperatura_c=estacao-01
    id_dispositivo=0 temperatura_c=estacao-02

  A entrada era um texto qualquer em id_dispositivo. O banco leu a
  tabela de CREDENCIAL e devolveu os dispositivos dela. O atacante
  nao quebrou nada: o servidor obedeceu a um comando que veio
  disfarçado de dado, e o resultado saiu pelo campo de resposta.
  A consulta por parametro nao achou nada, e era o resultado certo:
  nao existe dispositivo cujo id_dispositivo seja esse texto todo.
  O banco comparou a coluna com o texto inteiro e devolveu zero linha.

linhas com id_dispositivo que NAO existe na tabela de credencial: 0
Nenhuma: a consulta nao gravou nada, so leu o que nao devia.

--- o que o parametro NAO protege ---
  Ele nao verifica a faixa. Por parametro, 900 graus entra igual.
  Ele nao checa a autenticacao. Por parametro, o id_dispositivo
  continua sendo o que o cliente escreveu.
  Por isso sao DOIS muros: o parametro garante que o texto e dado;
  a validacao garante que o dado faz sentido; e a autenticacao
  garante que quem mandou tem permissao.

O professor aponta quatro coisas nessa saída, e as quatro são resultado da execução, não da explicação:

  • estacao-99, dispositivo que nao existe -> 401 dispositivo desconhecido: os três casos de recusa têm o mesmo status e motivos diferentes. Sem o motivo no corpo, os três viram um 401 só, e o aluno não sabe se esqueceu a chave ou se ela está errada. É o que o id do pedido do dia 8 aula 2 amarra.
  • temperatura_c=estacao-01: a coluna que o atacante não pediu. Ele pediu a segunda coluna da consulta, e o que voltou para a coluna do id_dispositivo foi o nome do dispositivo da tabela de credencial. O professor conta que essa linha é a credencial vazando, mesmo que o valor exibido pareça inofensivo.
  • EXECUTOU, devolveu 2 linha(s): a consulta não deu erro nenhum. Uma demonstração de ataque que dá erro não serve para nada, porque o aluno conclui que o banco é esperto. Ele não é: ele executa o que chega.
  • por parametro: devolveu 0 linha(s): zero é a resposta certa, e o aluno costuma achar que zero é falha. A entrada inteira é o texto do id_dispositivo procurado, e não existe dispositivo com esse nome. Se a consulta por parâmetro devolvesse alguma coisa, seria o sinal de que o driver não está protecting.

O sketch da placa, que é o lado do segundo muro do mesmo cabo:

// dia 9, aula 2: os dois muros.
//
// Autenticar e validar nao sao a mesma coisa, e o aluno confunde as duas o
// trimestre inteiro. A diferenca cabe numa frase:
//
//   o muro 1 (autenticar) responde "QUEM e voce?"
//   o muro 2 (validar)   responde "O QUE voce mandou?"
//
// Passar no primeiro nao garante o segundo. Quem passa em um esta
// autenticado e ainda assim pode mandar lixo; quem passa no segundo e nao
// passou no primeiro mandou coisa legitima de quem nao deveria mandar.
//
// Este sketch e a placa do lado do segundo muro. Ela tem a chave de
// autenticar (a senha de acesso a API) e a regra de faixa. O que ela NAO
// tem e a protecao do roteador, que e o primeiro muro.

#include <Arduino.h>
#include <DHT.h>
#include <HTTPClient.h>
#include <WiFi.h>

const int PIN_DHT = 4;
const uint8_t TIPO_SENSOR = DHT11;

// O MURO 1: a chave que a placa apresenta ao servidor. Valor FICTICIO.
// A chave de verdade morre no .env do Node (dia 11), nunca neste arquivo.
const char* CHAVE_API = "chave-ficticia-da-aula-09";

// O MURO 2: a faixa que faz sentido para o dado. Mesma regra do dia 8,
// repetida aqui de proposito: cada lado do cabo tem a sua copia.
const float MIN_TEMP_C = -40.0;
const float MAX_TEMP_C = 80.0;

const char* URL_SERVIDOR = "http://192.168.0.10:3000/api/leitura";

DHT sensor(PIN_DHT, TIPO_SENSOR);

struct Voto {
  const char* nome;
  const char* codigo;   // o que o servidor devolve
  bool permitido;
};

// Avalia os dois muros e diz qual deles barrou. Separar "nao tenho chave"
// de "chave boa, dado ruim" e o que permite responder 401 em um caso e
// 422 no outro — status diferentes, acoes diferentes no log.
Voto avaliar(float temperatura_c, bool tem_chave) {
  Voto v;

  if (!tem_chave || strlen(CHAVE_API) == 0) {
    v.nome = "sem chave no cabecalho";
    v.codigo = "401";
    v.permitido = false;
    return v;
  }

  if (isnan(temperatura_c) || temperatura_c < MIN_TEMP_C || temperatura_c > MAX_TEMP_C) {
    v.nome = "chave valida, dado fora de faixa";
    v.codigo = "422";
    v.permitido = false;
    return v;
  }

  v.nome = "os dois muros passaram";
  v.codigo = "201";
  v.permitido = true;
  return v;
}

// Sem concatenar o valor do usuario dentro do cabecalho: o cabecalho e
// montado com String, e o valor nunca e concatenado num comando de banco.
// A regra do segundo muro no lado do banco esta no dia 10.
void mostrarVoto(const char* cenario, float t, bool tem_chave) {
  Voto v = avaliar(t, tem_chave);

  Serial.print(v.permitido ? "pode gravar | " : "recusado  | ");
  Serial.print(v.codigo);
  Serial.print(" | ");
  Serial.println(cenario);
  Serial.print("            motivo: ");
  Serial.println(v.nome);
  Serial.print("            payload:");
  if (isnan(t)) {
    Serial.print("  (sem numero, sensor devolveu NaN)");
  } else {
    Serial.print("  temperatura_c=");
    Serial.print(t, 1);
  }
  Serial.println();
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("dia 9 aula 2 — autenticar e validar: os dois muros");
  Serial.println("===============================================");
  Serial.print("rota: ");
  Serial.println(URL_SERVIDOR);

  Serial.println();
  Serial.println("muro 1, autenticacao:  QUEM e voce?");
  Serial.println("  chave de API no cabecalho X-Chave. Vem do .env do Node.");
  Serial.println("  falhou aqui? 401, e o servidor nem olha o corpo.");
  Serial.println();
  Serial.println("muro 2, validacao:    O QUE voce mandou?");
  Serial.println("  tipo numerico, campo obrigatorio, faixa plausivel.");
  Serial.println("  falhou aqui? 422, a chave estava certa e o dado nao.");

  Serial.println();
  Serial.println("--- quatro requisicoes contra o mesmo servidor ---");

  mostrarVoto("chave boa + dado bom", 27.4, true);
  mostrarVoto("sem chave, dado bom", 27.4, false);
  mostrarVoto("chave boa + dado lixo", 900.0, true);
  mostrarVoto("sem chave + dado lixo", 900.0, false);

  Serial.println("Repare: 401 e 422 sao numeros diferentes porque sao");
  Serial.println("problemas diferentes. Um log que so diz \"deu erro\"");
  Serial.println("nao distingue a placa sem chave da placa com sensor quebrado,");
  Serial.println("e o professor perde meia hora procurando rede.");

  Serial.println();
  Serial.println("--- o que NAO e muro ---");
  Serial.println("  comparar o token com o do servidor e um muro.");
  Serial.println("  Concatenar o valor recebido dentro do SQL NAO e um muro: e uma");
  Serial.println("  porta dos fundos. O parametro `?` do driver e o muro de verdade.");
  Serial.println("  Regra da aula: o que se manda e o que se guarda sao coisas diferentes.");
  Serial.println("  O segundo muro vale para quem JÁ passou pelo primeiro.");

  // O WiFi fica desligado: o ponto da aula e a logica dos muros, e nao o
  // sinal. Ligar o WiFi aqui custaria 20 s de associate por nada.
  WiFi.mode(WIFI_OFF);
  Serial.println();
  Serial.println("WiFi desligado. O POST de verdade e do dia 5.");
}

void loop() {
  Serial.println();
  Serial.println("--- nova rodada ---");
  setup();
  delay(10000);
}

Por que assim e não de outro jeito. A função avaliar da placa checa o muro um antes do muro dois, e sai assim que um dos dois falha. A ordem não é estética: se o muro dois viesse primeiro, o caso "sem chave e dado lixo" devolveria 422 e o log diria que o problema foi o dado, quando o problema real foi a autenticação. Quem recebe o 422 vai caçar o DHT11, e o DHT11 está perfeito.

A placa não consulta banco nenhum para conferir a chave, e o comentário da CHAVE_API diz por quê: a chave de verdade morre no .env do Node, no dia 11. Na placa, a chave é uma constante fictícia que existe para o aluno ver o cabeçalho sendo montado e o 401 sendo tratado. A comparação de verdade acontece no servidor, e é o que o autenticar do script faz lendo a tabela de dispositivos.

Os quatro cenários do setup são a matriz completa dos dois muros, e o professor a desenha na lousa como quadrado de duas por duas: chave boa ou ausente, por dado bom ou lixo. A diagonal é o que interessa: sem chave + dado bom dá 401, e sem chave + dado lixo também dá 401. Nos dois, o problema é o mesmo, mesmo com dados opostos. Esse quadrado é o que separa autenticação de validação para a turma, e ele é o único diagrama da aula.

A função mostrarVoto imprime o motivo e o payload, e nessa ordem. O motivo vem primeiro porque é ele que o professor lê da mesa de dentro da sala, e o payload vem depois porque é ele que o aluno precisa para comparar com o que mandou. Um log que imprime o payload e não o motivo obriga a decodificar o JSON para descobrir o que aconteceu, e o dia 8 aula 2 já mostrou que essa é a receita de perder meia hora.

O WiFi.mode(WIFI_OFF) no fim economiza uns vinte segundos de tentativa de associação em trinta placas ao mesmo tempo, e o comentário diz que o POST de verdade é do dia 5. A aula é sobre a lógica dos muros, e a lógica roda sem sinal nenhum: ela roda no Serial diante da turma, que é o instrumento de aula.

Criterios de correcao

CritérioPontos
Tabela do item 2 preenchida nos cinco casos, com status e motivo2 pontos
DHT11 no GPIO4 com pull-up, e PIN_DHT e TIPO_SENSOR em const no topo1 pontos
Item 3: a diferença entre autenticação e autorização escrita em uma frase, com os dois exemplos2 pontos
Item 4: as duas linhas da consulta concatenada coladas, e a leitura de que a segunda é credencial2 pontos
Consulta do projeto trocada por consulta com parâmetro, com a saída de zero linha colada2 pontos
Item 6: os dois status comparados e o que muda entre eles1 pontos
Item 7: o 401 do dispositivo inexistente, e a leitura de que o motivo separa os casos1 pontos
Item 8: o ponto do projeto onde a checagem está depois de alterar o banco, e a troca proposta1 pontos

Erros comuns

ErroComo apareceCorreção
Concatenar valor do cliente dentro do SQL"Usei uma template string e funcionou""Funcionou porque o banco obedeceu. O ? do driver é o muro: o valor chega como dado, e nunca como parte do comando. Busca concatenada com LIKE também conta."
Concatenar com LIKE achando que é seguro"Uso % e ${valor}, mas filtro com LIKE""LIKE concatena do mesmo jeito que =. O ? também funciona dentro de LIKE: WHERE nome LIKE ?, com '%' + valor + '%' no array."
Usar query em vez de execute"Chamei pool.query com ?""Com ?, use execute. query não prepara o comando e o valor concatenado volta a virar texto. É a diferença entre a consulta segura e a que parece segura."
Autenticar depois do INSERT"A rota tem chave, mas só confiro no fim""Essa é a rota aberta por engano: a linha já foi gravada quando o 401 sai. Autentique antes de tocar no banco, e o corpo nem deve ser lido antes disso."
Usar o id_dispositivo do corpo para saber quem é"Peguei o dispositivo do JSON e gravei""O corpo não é confiável. O id_dispositivo tem que vir do cliente autenticado, e não do que o cliente escreveu. Sem isso, qualquer um grava em nome de qualquer placa."
Comparar a chave no código, com a chave hardcoded"A chave está no if e compara certo""A chave no if é a mesma credencial exposta do dia 9 aula 1, agora no servidor. Compare com o valor do banco, e o valor do banco vem do .env."
Devolver 422 para falta de chave"Chave errada e dado ruim são a mesma coisa""São muros diferentes. Falta de chave é 401 e o corpo não é lido; dado ruim é 422 e o corpo foi lido para ser julgado. Status diferente muda o que a placa faz."
Logar a chave recebida"Imprimi a chave para ver se chegou""O tamanho, como no dia 9 aula 1. A chave recebida no log vaza a credencial de novo, agora pelo lado do servidor."
401 sem motivo no corpo"O aluno perguntou por que foi recusado""O status agrupa, o motivo separa. sem chave no cabecalho, chave nao confere e dispositivo desconhecido são três consertos diferentes, e o log precisa dos três."
Achar que parâmetro protege faixa"Usei execute, então 900 graus não entra""O ? garante que o texto é dado, não que o dado faz sentido. Por parâmetro, 900 entra igual. É o segundo muro, e ele é outro muro."
Contar colunas errada no ataque de teste"Fiz o UNION e o banco recusou, então Injection não funciona""Recusou com ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT: as duas leituras precisam do mesmo número de colunas. O ataque funciona; a montagem é que estava errada."
Tratar erro e validação no mesmo catch"A validação virou 500""O 422 é regra de negócio e não é erro inesperado. Tratar os dois junto faz a placa repetir o envio para sempre, e o dia 8 aula 2 mostrou o que isso custa no log."
Confundir token com pessoa"A sessão é o token""A chave que a placa manda é token de máquina: não tem dono humano, e vale para sempre. Sessão é o que um usuário humano tem, e expira. É a abertura do 3o trimestre."

Desafio extra

Escreva a rota que autentica, valida e grava, na ordem, e que devolve 401, 422 e 500 em três casos distintos com o motivo no corpo. Depois escreva a mesma rota com a checagem de autenticação depois do INSERT, rode as duas com o mesmo pedido sem chave, e compare quantas linhas sobraram na tabela. A diferença entre as duas é o que a ordem dos muros custa, e ela aparece no número de linhas, não em opinião.

>

A resolucao, compilada

// dia 9, aula 2: os dois muros.
//
// Autenticar e validar nao sao a mesma coisa, e o aluno confunde as duas o
// trimestre inteiro. A diferenca cabe numa frase:
//
//   o muro 1 (autenticar) responde "QUEM e voce?"
//   o muro 2 (validar)   responde "O QUE voce mandou?"
//
// Passar no primeiro nao garante o segundo. Quem passa em um esta
// autenticado e ainda assim pode mandar lixo; quem passa no segundo e nao
// passou no primeiro mandou coisa legitima de quem nao deveria mandar.
//
// Este sketch e a placa do lado do segundo muro. Ela tem a chave de
// autenticar (a senha de acesso a API) e a regra de faixa. O que ela NAO
// tem e a protecao do roteador, que e o primeiro muro.

#include <Arduino.h>
#include <DHT.h>
#include <HTTPClient.h>
#include <WiFi.h>

const int PIN_DHT = 4;
const uint8_t TIPO_SENSOR = DHT11;

// O MURO 1: a chave que a placa apresenta ao servidor. Valor FICTICIO.
// A chave de verdade morre no .env do Node (dia 11), nunca neste arquivo.
const char* CHAVE_API = "chave-ficticia-da-aula-09";

// O MURO 2: a faixa que faz sentido para o dado. Mesma regra do dia 8,
// repetida aqui de proposito: cada lado do cabo tem a sua copia.
const float MIN_TEMP_C = -40.0;
const float MAX_TEMP_C = 80.0;

const char* URL_SERVIDOR = "http://192.168.0.10:3000/api/leitura";

DHT sensor(PIN_DHT, TIPO_SENSOR);

struct Voto {
  const char* nome;
  const char* codigo;   // o que o servidor devolve
  bool permitido;
};

// Avalia os dois muros e diz qual deles barrou. Separar "nao tenho chave"
// de "chave boa, dado ruim" e o que permite responder 401 em um caso e
// 422 no outro — status diferentes, acoes diferentes no log.
Voto avaliar(float temperatura_c, bool tem_chave) {
  Voto v;

  if (!tem_chave || strlen(CHAVE_API) == 0) {
    v.nome = "sem chave no cabecalho";
    v.codigo = "401";
    v.permitido = false;
    return v;
  }

  if (isnan(temperatura_c) || temperatura_c < MIN_TEMP_C || temperatura_c > MAX_TEMP_C) {
    v.nome = "chave valida, dado fora de faixa";
    v.codigo = "422";
    v.permitido = false;
    return v;
  }

  v.nome = "os dois muros passaram";
  v.codigo = "201";
  v.permitido = true;
  return v;
}

// Sem concatenar o valor do usuario dentro do cabecalho: o cabecalho e
// montado com String, e o valor nunca e concatenado num comando de banco.
// A regra do segundo muro no lado do banco esta no dia 10.
void mostrarVoto(const char* cenario, float t, bool tem_chave) {
  Voto v = avaliar(t, tem_chave);

  Serial.print(v.permitido ? "pode gravar | " : "recusado  | ");
  Serial.print(v.codigo);
  Serial.print(" | ");
  Serial.println(cenario);
  Serial.print("            motivo: ");
  Serial.println(v.nome);
  Serial.print("            payload:");
  if (isnan(t)) {
    Serial.print("  (sem numero, sensor devolveu NaN)");
  } else {
    Serial.print("  temperatura_c=");
    Serial.print(t, 1);
  }
  Serial.println();
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("dia 9 aula 2 — autenticar e validar: os dois muros");
  Serial.println("===============================================");
  Serial.print("rota: ");
  Serial.println(URL_SERVIDOR);

  Serial.println();
  Serial.println("muro 1, autenticacao:  QUEM e voce?");
  Serial.println("  chave de API no cabecalho X-Chave. Vem do .env do Node.");
  Serial.println("  falhou aqui? 401, e o servidor nem olha o corpo.");
  Serial.println();
  Serial.println("muro 2, validacao:    O QUE voce mandou?");
  Serial.println("  tipo numerico, campo obrigatorio, faixa plausivel.");
  Serial.println("  falhou aqui? 422, a chave estava certa e o dado nao.");

  Serial.println();
  Serial.println("--- quatro requisicoes contra o mesmo servidor ---");

  mostrarVoto("chave boa + dado bom", 27.4, true);
  mostrarVoto("sem chave, dado bom", 27.4, false);
  mostrarVoto("chave boa + dado lixo", 900.0, true);
  mostrarVoto("sem chave + dado lixo", 900.0, false);

  Serial.println("Repare: 401 e 422 sao numeros diferentes porque sao");
  Serial.println("problemas diferentes. Um log que so diz \"deu erro\"");
  Serial.println("nao distingue a placa sem chave da placa com sensor quebrado,");
  Serial.println("e o professor perde meia hora procurando rede.");

  Serial.println();
  Serial.println("--- o que NAO e muro ---");
  Serial.println("  comparar o token com o do servidor e um muro.");
  Serial.println("  Concatenar o valor recebido dentro do SQL NAO e um muro: e uma");
  Serial.println("  porta dos fundos. O parametro `?` do driver e o muro de verdade.");
  Serial.println("  Regra da aula: o que se manda e o que se guarda sao coisas diferentes.");
  Serial.println("  O segundo muro vale para quem JÁ passou pelo primeiro.");

  // O WiFi fica desligado: o ponto da aula e a logica dos muros, e nao o
  // sinal. Ligar o WiFi aqui custaria 20 s de associate por nada.
  WiFi.mode(WIFI_OFF);
  Serial.println();
  Serial.println("WiFi desligado. O POST de verdade e do dia 5.");
}

void loop() {
  Serial.println();
  Serial.println("--- nova rodada ---");
  setup();
  delay(10000);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia09 aula2.