Seguranca além de SQL injection — Arduino e IoT — semana 9 do 3o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 9 · Seguranca além de SQL injection — Material de Apoio Arduino

Semana 9 de 15· 3o trimestre · 05/09 a 10/12

Seguranca além de SQL injection

O resto do que uma coisa conectada precisa e ainda não tem.

Aula 1 — Entrada não confiavel em toda parte

Objetivos

  • Apontar, num sistema do ano, os quatro lugares onde a entrada não confiável entra: sensor, rede, formulário e resposta do servidor.
  • Reconhecer no painel do projeto o ponto exato em que uma leitura vira innerHTML, trocar por textContent e nomear o defeito: XSS.
  • Escapar a saída em dois destinos diferentes, o texto que vira JSON e o texto que vira página, e explicar por que a lista de caracteres muda de um para o outro.
  • Tratar a recusa do servidor por código de status, em vez de repetir o envio como se nada tivesse acontecido.
  • Rodar npm audit no projeto do trimestre e dizer, em uma frase, o que um número de vulnerabilidade muda no seu trabalho.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 computador por dupla, com Node 20 ou superior
  • Projeto do trimestre 3, com package.json e package-lock.json versionados
  • Terminal com npm instalado, para o npm audit do item 5
  • O painel do projeto aberto em uma aba e o servidor em outra, para o teste de CORS e de CSRF

Conceitos

Quatro origens, uma única regra

A entrada não confiável não é um problema do navegador nem do banco. É um problema de segurança de todo lugar por onde o dado entra, e o curso inteiro converge aqui. No segundo trimestre a SQL injection fechou a porta do banco: consulta parametrizada, e o banco deixou de interpretar dado como comando. O que sobrou foram as outras três portas.

OrigemQuem mandaO que pode acontecer
Sensoro device, com defeitonúmero fora do limite, texto no lugar de número, dado gigante
Formulárioa pessoa na telaqualquer string, inclusive "; DROP TABLE e <script>
Resposta de servidora máquina do outro ladoresposta enorme, JSON inválido, código de erro inesperado
Cabeçalhoa rede intermediáriavalor adulterado em caminho não confiável

A regra é uma só, e ela vale para as quatro linhas: quem produz o dado não pode ser quem decide que o dado é válido. No banco isso virou consulta parametrizada. No JSON vira escapar. No HTML vira textContent.

XSS: injeção de HTML no navegador de quem está logado

O nome completo é cross-site scripting, a sigla é XSS, e o defeito tem um caráter anatômico: ele não acontece no servidor. O servidor recebe o dado, grava no banco, devolve a página e sai tudo certo do ponto de vista dele. O código malicioso roda depois, no navegador de outra pessoa, no momento em que o navegador le a resposta. Por isso ele escapa de todo teste que roda no servidor: o servidor, sozinho, não consegue ver o defeito.

O caminho tem quatro elos, e quebrar um basta:

  1. A pessoa escreve um valor no formulário.
  2. O servidor guarda sem validar o formato.
  3. O painel do projeto lê o valor e escreve num innerHTML.
  4. O navegador encontra <script> e executa.

O elo 3 é o que se chama injeção de HTML, e o nome vem de ser exatamente o que é: o dado entrou dentro do HTML como se fosse markup. A defesa padrão do navegador é não depender do item 3: textContent escreve o valor como texto, e o navegador não interpreta nada. Quando o servidor precisa montar a página em string, ai entra escapar saída: cada caractere especial vira entidade, e o resultado é visualmente igual e semanticamente inerte.

Duas palavras que o aluno sempre troca. Validar decide se o dado tem o formato aceito. Sanitizar é reescrever o dado para tirar o que é perigoso. Validar e escapar são passos diferentes, com ordem: validar na entrada, escapar na saída. Quem só valida deixa passar o valor bem formado e ainda perigoso, como sala-01"><script>alert(1)</script>, que passa em qualquer regra de comprimento e de formato.

Regra do dia, e ela cabe numa linha: dado de usuário nunca entra em innerHTML. Nem "por enquanto", nem "esse campo é interno", nem "esse valor já passou pelo banco".

CORS: a origem, e CSRF: a requisição forjada

CORS não é proteção do servidor. É um pedido do navegador ao servidor, perguntando se outra origem pode ler a resposta. Quem protege o servidor é a checagem de permissão no próprio caminho do recurso, e a aula 2 do dia 3 já mostrou isso com o token.

O erro clássico é o asterisco. Access-Control-Allow-Origin: * junto com Access-Control-Allow-Credentials: true e recusado pelo navegador, porque significa "qualquer site do planeta, com o seu cookie de login". A forma correta tem duas partes: a origem permitida vem de configuração, CORS_ORIGIN=http://localhost:3000, e o servidor devolve exatamente aquele valor. localhost no desenvolvimento e o domínio próprio na produção, e a variável muda entre os dois sem tocar no código.

CSRF é o outro lado, e o nome completo dele é requisição forjada: um site que a pessoa abriu numa outra aba dispara um POST no seu servidor usando o cookie de sessão que o navegador já tem. O navegador age, e a pessoa não pediu nada. O token anti-CSRF resolve porque a requisição forjada não tem o token: o formulário traz o token no corpo e no cabeçalho, e o servidor compara. A segunda defesa é o cookie SameSite, que é o atributo do cabeçalho Set-Cookie que decide se o navegador acompanha o pedido para fora do próprio site. Lax sozinho resolve quase tudo; o token resolve o resto.

Dependência vulnerável, versão antiga e npm audit

npm audit lê o package-lock.json e cruza as versões com o banco de vulnerabilidades público. O comando que roda no projeto do trimestre é npm audit --omit=dev, porque devDependencies não vão para produção: um problema ali não affects o servidor em si.

O segundo item da saída importa mais que o primeiro. found 0 vulnerabilities e found 3 vulnerabilities dizem o que existe; o que muda o seu trabalho é a linha de cada pacote, com a versão antiga apontada e o caminho de correção. Uma dependência vulnerável em versão antiga não é problema de teoria: é a única forma de um resumo de CVE entrar no seu servidor, e ninguém te avisou.

O caminho de correção tem duas saídas. npm audit fix troca a versão e não olha seu código, então pode subir a versão maior e quebrar a API. A alternativa manual é trocar a linha do package.json para a faixa corrigida, rodar os testes, e só então aceitar. O professor roda o comando na frente da turma de propósito, mesmo com zero: o aluno precisa ver a ferramenta funcionando num projeto limpo antes de precisar dela num projeto sujo.

Atividade

Montagem: nenhuma. Esta aula roda inteira no monitor serial e no terminal, sem circuito. A placa só precisa do cabo USB.

  1. Abra o projeto do trimestre 3 e liste, no caderno, os quatro pontos de entrada da tabela do roteiro. Para cada um, escreva o nome do arquivo onde ele entra. Um ponto que você não conseguir nomear está quebrado.
  2. Grave o sketch da resolução e rode no monitor serial a 115200. Anote as três formas do mesmo valor: o texto cru, o texto depois de escapar para JSON e o texto depois de escaparHtml para página.
  3. No painel do projeto, ache uma ocorrência de innerHTML que receba dado de usuário e troque por textContent. Se não houver nenhuma, escreva onde a saída do banco entra no HTML e diga por que lá ela não é perigosa.
  4. Adicione um escapeHtml no servidor, no ponto exato em que a leitura vira linha de log, e mostre a diferença do log com uma leitura que contenha <b>.
  5. Rode npm audit --omit=dev no projeto e anote o número de vulnerabilidades. Se for zero, escreva o comando que encontra uma dependência com problema conhecido.
  6. Em três linhas: por que Access-Control-Allow-Origin: * não protege o servidor, e o que o professor deveria checar no lugar dele.
  7. Em três linhas: o que é uma requisição forjada, e qual defesa a placa do projeto não tem hoje.

Nota: 12 pontos. Critério de fim: o serial mostra o mesmo valor em três formas, uma crua e duas escapadas, e o npm audit rodou com o número anotado.

Resolucao

// Aula 1 do dia 9: entrada nao confiavel em toda parte.
//
// A placa e a fronteira do sistema. Tudo que chega nela vem de um sensor que
// pode estar com defeito, de uma rede que qualquer um pode usar e de um
// firmware que o dono le da loja. Nesta aula o firmware para de tratar a
// entrada como se fosse boa.
//
// O codigo mostra a placa montando o pedido HTTP na mao, caractere a
// caractere. E assim porque o aluno precisa VER onde entra a entrada nao
// confiavel: se a biblioteca faz tudo, ele nao tem nada para escapar.

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

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Endereco que existe so para o exemplo. A aula roda sem servidor: o que
// importa e a montagem do pedido e o tratamento da resposta.
const char* URL_SERVIDOR = "http://exemplo.invalido/leitura";

// O que a placa INVENTA no pedido: o identificador e a versao do firmware.
const char* ID_DISPOSITIVO = "esp32-bancada-01";
const char* VERSAO_FIRMWARE = "1.4.0";

// Numero maximo de bytes que aceitamos na resposta. Um servidor que devolve
// 50 MB de uma vez acaba com a memoria de uma placa que tem 320 KB de RAM.
const unsigned long TAMANHO_MAXIMO_RESPOSTA = 2048;

// Limite de bytes para qualquer campo que venha de fora. E o primeiro filtro:
// antes de perguntar se o dado e valido, a placa recusa o que e grande demais
// para caber nela.
const unsigned int TAMANHO_MAXIMO_CAMPO = 32;

WiFiClient cliente;

// Um numero de sensor precisa ter digito, e no maximo um separador, e nenhum
// espaco no meio. Escrever o check na mao e o ponto da aula: a biblioteca
// faria mais rapido e o aluno nao veria o que esta sendo recusado.
bool ehNumeroDeSensor(const String& texto) {
  if (texto.length() == 0 || texto.length() > TAMANHO_MAXIMO_CAMPO) {
    return false;
  }
  int separadores = 0;
  for (unsigned int i = 0; i < texto.length(); i++) {
    char c = texto.charAt(i);
    if (c == '.' || c == ',') {
      separadores++;
      if (separadores > 1) {
        return false;
      }
      continue;
    }
    if (c < '0' || c > '9') {
      return false;
    }
  }
  return true;
}

// ---------------------------------------------------------------- utilidades

// Escapa um valor que veio de fora antes de entrar num texto que vai virar
// JSON. Sem isto, um sensor que devolve 23.5" ou um campo chamado
// <script> entra no arquivo de log como codigo, e nao como texto.
String escapar(const String& entrada) {
  String saida;
  saida.reserve(entrada.length() + 16);
  for (unsigned int i = 0; i < entrada.length(); i++) {
    char c = entrada.charAt(i);
    if (c == '"' || c == '\\') {
      saida += '\\';
      saida += c;
    } else if (c == '\n') {
      saida += "\\n";
    } else if (c == '\r') {
      saida += "\\r";
    } else if (c == '\t') {
      saida += "\\t";
    } else if (c < 0x20) {
      saida += ' ';
    } else {
      saida += c;
    }
  }
  return saida;
}

// Segunda funcao, e a que o professor vai pedir no dia: escapar a saida de
// HTML. E a defesa contra o defeito que o painel executa no NAVEGADOR de quem
// esta logado, sem tocar no servidor. Repare que a lista de caracteres e maior
// que a do JSON: entra tambem o apostrofo, porque o valor pode cair dentro de
// um atributo com aspas simples.
String escaparHtml(const String& entrada) {
  String saida;
  saida.reserve(entrada.length() + 32);
  for (unsigned int i = 0; i < entrada.length(); i++) {
    char c = entrada.charAt(i);
    switch (c) {
      case '<': saida += "&lt;"; break;
      case '>': saida += "&gt;"; break;
      case '&': saida += "&amp;"; break;
      case '"': saida += "&quot;"; break;
      case '\'': saida += "&#39;"; break;
      default: saida += c; break;
    }
  }
  return saida;
}

// Monta o JSON na mao, campo a campo, escapando o que veio do sensor. A
// biblioteca JSONResolver tambem escapa, e por isso ela e a escolha melhor no
// projeto final; aqui a montagem e manual de proposito, para o aluno ver
// exatamente onde a entrada entra.
String montarPayload(const String& leituraBruta) {
  String payload = "{";
  payload += "\"device_id\":\"" + String(ID_DISPOSITIVO) + "\",";
  payload += "\"firmware\":\"" + String(VERSAO_FIRMWARE) + "\",";
  payload += "\"leitura\":\"" + escapar(leituraBruta) + "\",";
  payload += "\"unidade\":\"C\"";
  payload += "}";
  return payload;
}

// ------------------------------------------------------------------- a placa

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println();
  Serial.println("=== dia 9, aula 1: entrada nao confiavel ===");
  Serial.println("Nenhuma rede precisa estar de pe para rodar esta aula.");
  Serial.println();

  // O sketch nao abre socket: instancia o cliente, que e o transporte HTTP do
  // exemplo, e configura o timeout. Tudo mais no codigo e sobre o que fazer
  // com o texto que chega depois.
  Serial.println("[placa] WiFi Client ativado.");
  cliente.setTimeout(3000);
  Serial.println("[placa] timeout do cliente: 3 s");
  Serial.println();

  // 1. Entrada que vem do sensor: tem formato esperado e tem limite.
  String sensorBom = "23.5";
  String sensorRuim = "23.5C ou erro";
  Serial.println("[sensor] caso bom: " + sensorBom +
                 " -> aceito: " + String(ehNumeroDeSensor(sensorBom) ? "sim" : "nao"));
  Serial.println("[sensor] caso ruim: " + sensorRuim +
                 " -> aceito: " + String(ehNumeroDeSensor(sensorRuim) ? "sim" : "nao"));
  Serial.println("[sensor] texto gigante de 40 bytes -> aceito: " +
                 String(ehNumeroDeSensor(String(40, '9')) ? "sim" : "nao"));
  Serial.println();

  // 2. Entrada que vem da rede: e a que o aluno nao controla.
  String campoDoUsuario = "sala-01\"><script>alert(1)</script>";
  Serial.println("[rede] campo recebido de um formulario:");
  Serial.println("[rede]   " + campoDoUsuario);
  Serial.println("[rede] contem aspa: " +
                 String(campoDoUsuario.indexOf("\"") >= 0 ? "sim" : "nao"));
  Serial.print("[rede] depois de escapar: ");
  Serial.println(escapar(campoDoUsuario));
  Serial.print("[navegador] HTML injection, mesmo valor depois de escapar saida: ");
  Serial.println(escaparHtml(campoDoUsuario));
  Serial.println("[navegador] o <script> virou texto: o navegador nao executa mais.");
  Serial.println();

  // 3. O payload final, ja com tudo escapado.
  String payload = montarPayload(campoDoUsuario);
  Serial.println("[payload] JSON que iria para o servidor:");
  Serial.println("[payload]   " + payload);
  Serial.println("[payload] tamanho: " + String(payload.length()) + " bytes");
  Serial.println();

  // 4. O que a placa faz se o servidor nao responde. Esta e a parte que
  //    o aluno esquece: construir o pedido e facil, tratar a falha e a aula.
  Serial.println("[envio] nenhuma requisicao real e feita nesta aula.");
  Serial.println("[envio] URL alvo: " + String(URL_SERVIDOR));
  Serial.println("[envio] o servidor, na aula real, responderia com codigo de status.");
  Serial.println("[envio] 401 e 403 significam token recusado: a placa guarda o token");
  Serial.println("[envio] e o servidor e que decidiu que ele nao vale mais.");
  Serial.println("[envio] a placa nao tem como corrigir isso sozinha: so avisar.");
  Serial.println();

  // 5. O limite de resposta e a defesa contra a placa travar.
  Serial.println("[memoria] limite de resposta: " +
                 String(TAMANHO_MAXIMO_RESPOSTA) + " bytes");
  Serial.println("[memoria] sem esse limite, uma resposta enorme esvazia a RAM da placa.");
  Serial.println();
  Serial.println("[placa] heap livre: " + String(ESP.getFreeHeap()) + " bytes");
  Serial.println("[fim]");
}

void loop() {
  delay(5000);
  Serial.println("[placa] viva. heap livre: " + String(ESP.getFreeHeap()) + " bytes");
}

Por que assim e não de outro jeito. O sketch não faz requisição nenhuma. Isso é deliberado, e o professor precisa dizer em voz alta: se a aula depende de um servidor no ar, metade da turma fica esperando e a outra metade desiste no primeiro erro de rede. O que o sketch faz é mostrar os três estados do mesmo dado, cru, recusado e escapado, e isso não depende de ninguém.

A montagem do JSON na mão é o segundo ponto. Existindo JSONResolver na plataforma, a escolha "correta" é usar a biblioteca; a escolha didática é montar na mão, porque o aluno precisa ver o ponto exato onde a entrada entra no texto. O professor diz isso explicitamente e aponta que no projeto final o JSONResolver volta.

A escaparHtml entra pelo mesmo motivo e por um segundo motivo pedagógico: as duas listas de caracteres são diferentes, e isso é o que a turma costuma errar. O JSON precisa de cinco escapes, e o HTML precisa de cinco também, mas não são os cinco mesmos: entra o apóstrofo, porque o valor pode cair dentro de um atributo com aspas simples. Mesmo código de estrutura, contexto diferente, resposta diferente.

O ehNumeroDeSensor existe pelo mesmo motivo que as duas funções de escape: nenhum dos três é a solução de produção, e os três são a única forma de o aluno ver a regra funcionando. A função tem um limite de tamanho antes da checagem de formato, e essa ordem é o ponto: o filtro barato vem antes do caro.

O TAMANHO_MAXIMO_RESPOSTA não é lenda: uma placa com 320 KB de RAM que aceita resposta sem limite trava quando o servidor errar a mão e devolver um HTML de página de erro de 2 MB. Limite de entrada é segurança, e vale para os dois lados.

A função escapar reserva memória com reserve porque String realoca a cada +=, e realocar cinco vezes num dispositivo com pouca RAM é o que faz o programa ficar lento sem erro nenhum.

Criterios de correcao

CritérioPontos
Os quatro pontos de entrada listados com o nome do arquivo de cada um3 pontos
Serial mostra o valor cru e as duas versões escapadas, com a diferença explicada3 pontos
innerHTML com dado de usuário trocado por textContent, ou justificativa de por que não havia2 pontos
escapeHtml no servidor, com a diferença do log mostrada em uma leitura que contêm <b>2 pontos
npm audit --omit=dev executado e número de vulnerabilidades anotado1 ponto
Itens 6 e 7 respondidos: CORS não protege o servidor, e a requisição forjada está nomeada1 ponto

Erros comuns

ErroComo apareceCorreção
Escapar só quando o valor "parece" perigosoCódigo com if (texto.contains("<")) antes de escapar"Escapa sempre. Se você precisa decidir se o valor é perigoso, você já perdeu: o valor novo vai burlar a sua condição."
Validar e não escapar, achando que validar protege a saídaO formulário aceita sala-01"><script>alert(1)</script> porque o valor "está no formato certo""Validar decide o formato, escapar decide a sobrevivencia no sistema que vai imprimir. O seu valor está no formato certo e continua sendo um ataque."
escapeHtml aplicado duas vezesO painel mostra &lt;b&gt; em vez de <b>"Escapar duas vezes produz texto que parece escapado e não é. Escape uma vez, na saída."
innerHTML porque "o dado já vem do banco"O aluno argumenta que o banco não guarda <script>"O dado passa por um formulário antes de chegar no banco, e esse é o caminho. Além disso: quem escreve no banco pode não ser quem você pensa."
CSRF tratado como "assunto do navegador"O aluno desabilita o token e diz que o navegador resolve"O token anti-CSRF não é opção do navegador: ele não existe na requisição forjada. É o servidor que compara. Sem ele, o cookie sozinho entrega a sessão."
Access-Control-Allow-Origin: * como "liberar o acesso"O aluno pergunta como liberar a API para o painel"Asterisco e navegador recusando credencial. O que você quer dizer é 'aceito o painel do domínio X', e o domínio X vai em variável de ambiente."
npm audit sem --omit=devO aluno conta vulnerabilidade do nodemon como se fosse do servidor"nodemon não sobe para produção. Use npm audit --omit=dev, ou o número não descreve o que roda no servidor."
npm audit fix sem rodar os testesO package.json sobe de versão maior e o painel quebra em outra tela"O fix troca versão sem olhar seu código. Troque a faixa, rode os testes, e só então aceite."
Ignorar a resposta e repetir o enviowhile (!resposta) { enviar(); } no sketch"Gastar a bateria reenviando para um servidor que já disse não é o pior caso. 401 e recusa: pare e avise."
Nunca ouvir o status HTTPO código lê o corpo e ignora o 201 e o 401"O status é a resposta. 201 gravou, 409 regra de negócio, 401 token recusado, 500 foi o servidor. O corpo explica; o status decide."

Desafio extra

A escapar desta aula trata cinco caracteres do JSON, e a escaparHtml trata cinco do HTML, e nenhuma das duas é a lista completa. Escreva a versão que trata os dois conjuntos e calcule, no caderno, quantos bytes a mais o log ocupa por leitura quando todos os caracteres são escapados. Depois mude o servidor para textContent e meca quanto o painel carregou a menos. O número da segunda medição é o que você vai defender na apresentação do projeto.

>

A resolucao, compilada

// Aula 1 do dia 9: entrada nao confiavel em toda parte.
//
// A placa e a fronteira do sistema. Tudo que chega nela vem de um sensor que
// pode estar com defeito, de uma rede que qualquer um pode usar e de um
// firmware que o dono le da loja. Nesta aula o firmware para de tratar a
// entrada como se fosse boa.
//
// O codigo mostra a placa montando o pedido HTTP na mao, caractere a
// caractere. E assim porque o aluno precisa VER onde entra a entrada nao
// confiavel: se a biblioteca faz tudo, ele nao tem nada para escapar.

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

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Endereco que existe so para o exemplo. A aula roda sem servidor: o que
// importa e a montagem do pedido e o tratamento da resposta.
const char* URL_SERVIDOR = "http://exemplo.invalido/leitura";

// O que a placa INVENTA no pedido: o identificador e a versao do firmware.
const char* ID_DISPOSITIVO = "esp32-bancada-01";
const char* VERSAO_FIRMWARE = "1.4.0";

// Numero maximo de bytes que aceitamos na resposta. Um servidor que devolve
// 50 MB de uma vez acaba com a memoria de uma placa que tem 320 KB de RAM.
const unsigned long TAMANHO_MAXIMO_RESPOSTA = 2048;

// Limite de bytes para qualquer campo que venha de fora. E o primeiro filtro:
// antes de perguntar se o dado e valido, a placa recusa o que e grande demais
// para caber nela.
const unsigned int TAMANHO_MAXIMO_CAMPO = 32;

WiFiClient cliente;

// Um numero de sensor precisa ter digito, e no maximo um separador, e nenhum
// espaco no meio. Escrever o check na mao e o ponto da aula: a biblioteca
// faria mais rapido e o aluno nao veria o que esta sendo recusado.
bool ehNumeroDeSensor(const String& texto) {
  if (texto.length() == 0 || texto.length() > TAMANHO_MAXIMO_CAMPO) {
    return false;
  }
  int separadores = 0;
  for (unsigned int i = 0; i < texto.length(); i++) {
    char c = texto.charAt(i);
    if (c == '.' || c == ',') {
      separadores++;
      if (separadores > 1) {
        return false;
      }
      continue;
    }
    if (c < '0' || c > '9') {
      return false;
    }
  }
  return true;
}

// ---------------------------------------------------------------- utilidades

// Escapa um valor que veio de fora antes de entrar num texto que vai virar
// JSON. Sem isto, um sensor que devolve 23.5" ou um campo chamado
// <script> entra no arquivo de log como codigo, e nao como texto.
String escapar(const String& entrada) {
  String saida;
  saida.reserve(entrada.length() + 16);
  for (unsigned int i = 0; i < entrada.length(); i++) {
    char c = entrada.charAt(i);
    if (c == '"' || c == '\\') {
      saida += '\\';
      saida += c;
    } else if (c == '\n') {
      saida += "\\n";
    } else if (c == '\r') {
      saida += "\\r";
    } else if (c == '\t') {
      saida += "\\t";
    } else if (c < 0x20) {
      saida += ' ';
    } else {
      saida += c;
    }
  }
  return saida;
}

// Segunda funcao, e a que o professor vai pedir no dia: escapar a saida de
// HTML. E a defesa contra o defeito que o painel executa no NAVEGADOR de quem
// esta logado, sem tocar no servidor. Repare que a lista de caracteres e maior
// que a do JSON: entra tambem o apostrofo, porque o valor pode cair dentro de
// um atributo com aspas simples.
String escaparHtml(const String& entrada) {
  String saida;
  saida.reserve(entrada.length() + 32);
  for (unsigned int i = 0; i < entrada.length(); i++) {
    char c = entrada.charAt(i);
    switch (c) {
      case '<': saida += "&lt;"; break;
      case '>': saida += "&gt;"; break;
      case '&': saida += "&amp;"; break;
      case '"': saida += "&quot;"; break;
      case '\'': saida += "&#39;"; break;
      default: saida += c; break;
    }
  }
  return saida;
}

// Monta o JSON na mao, campo a campo, escapando o que veio do sensor. A
// biblioteca JSONResolver tambem escapa, e por isso ela e a escolha melhor no
// projeto final; aqui a montagem e manual de proposito, para o aluno ver
// exatamente onde a entrada entra.
String montarPayload(const String& leituraBruta) {
  String payload = "{";
  payload += "\"device_id\":\"" + String(ID_DISPOSITIVO) + "\",";
  payload += "\"firmware\":\"" + String(VERSAO_FIRMWARE) + "\",";
  payload += "\"leitura\":\"" + escapar(leituraBruta) + "\",";
  payload += "\"unidade\":\"C\"";
  payload += "}";
  return payload;
}

// ------------------------------------------------------------------- a placa

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println();
  Serial.println("=== dia 9, aula 1: entrada nao confiavel ===");
  Serial.println("Nenhuma rede precisa estar de pe para rodar esta aula.");
  Serial.println();

  // O sketch nao abre socket: instancia o cliente, que e o transporte HTTP do
  // exemplo, e configura o timeout. Tudo mais no codigo e sobre o que fazer
  // com o texto que chega depois.
  Serial.println("[placa] WiFi Client ativado.");
  cliente.setTimeout(3000);
  Serial.println("[placa] timeout do cliente: 3 s");
  Serial.println();

  // 1. Entrada que vem do sensor: tem formato esperado e tem limite.
  String sensorBom = "23.5";
  String sensorRuim = "23.5C ou erro";
  Serial.println("[sensor] caso bom: " + sensorBom +
                 " -> aceito: " + String(ehNumeroDeSensor(sensorBom) ? "sim" : "nao"));
  Serial.println("[sensor] caso ruim: " + sensorRuim +
                 " -> aceito: " + String(ehNumeroDeSensor(sensorRuim) ? "sim" : "nao"));
  Serial.println("[sensor] texto gigante de 40 bytes -> aceito: " +
                 String(ehNumeroDeSensor(String(40, '9')) ? "sim" : "nao"));
  Serial.println();

  // 2. Entrada que vem da rede: e a que o aluno nao controla.
  String campoDoUsuario = "sala-01\"><script>alert(1)</script>";
  Serial.println("[rede] campo recebido de um formulario:");
  Serial.println("[rede]   " + campoDoUsuario);
  Serial.println("[rede] contem aspa: " +
                 String(campoDoUsuario.indexOf("\"") >= 0 ? "sim" : "nao"));
  Serial.print("[rede] depois de escapar: ");
  Serial.println(escapar(campoDoUsuario));
  Serial.print("[navegador] HTML injection, mesmo valor depois de escapar saida: ");
  Serial.println(escaparHtml(campoDoUsuario));
  Serial.println("[navegador] o <script> virou texto: o navegador nao executa mais.");
  Serial.println();

  // 3. O payload final, ja com tudo escapado.
  String payload = montarPayload(campoDoUsuario);
  Serial.println("[payload] JSON que iria para o servidor:");
  Serial.println("[payload]   " + payload);
  Serial.println("[payload] tamanho: " + String(payload.length()) + " bytes");
  Serial.println();

  // 4. O que a placa faz se o servidor nao responde. Esta e a parte que
  //    o aluno esquece: construir o pedido e facil, tratar a falha e a aula.
  Serial.println("[envio] nenhuma requisicao real e feita nesta aula.");
  Serial.println("[envio] URL alvo: " + String(URL_SERVIDOR));
  Serial.println("[envio] o servidor, na aula real, responderia com codigo de status.");
  Serial.println("[envio] 401 e 403 significam token recusado: a placa guarda o token");
  Serial.println("[envio] e o servidor e que decidiu que ele nao vale mais.");
  Serial.println("[envio] a placa nao tem como corrigir isso sozinha: so avisar.");
  Serial.println();

  // 5. O limite de resposta e a defesa contra a placa travar.
  Serial.println("[memoria] limite de resposta: " +
                 String(TAMANHO_MAXIMO_RESPOSTA) + " bytes");
  Serial.println("[memoria] sem esse limite, uma resposta enorme esvazia a RAM da placa.");
  Serial.println();
  Serial.println("[placa] heap livre: " + String(ESP.getFreeHeap()) + " bytes");
  Serial.println("[fim]");
}

void loop() {
  delay(5000);
  Serial.println("[placa] viva. heap livre: " + String(ESP.getFreeHeap()) + " bytes");
}

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

Aula 2 — Segredo, transporte e o dispositivo na rede

Objetivos

  • Mapear, para cada segredo do projeto, os lugares onde ele pode existir e quem escreve em cada um: sketch versionado, variável de ambiente em produção, servidor de deploy e memória persistente da placa.
  • Tirar o segredo do firmware, gravá-lo na memória persistente pela porta do pareamento e dizer por que o .ino continua podendo ir para o repositório inteiro.
  • Ler no monitor serial a sequência pareamento, boot, 201 e 401, e dizer em qual dos quatro estados a placa para de enviar.
  • Reescrever uma linha de log real do projeto sem cabeçalho Authorization e sem log com IP do solicitante, mantendo o que ainda serve para diagnosticar.
  • Justificar por que cifrar o transporte resolve a leitura no caminho da rede e não resolve o log, nem o Serial, nem o repositório.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • Computador com o projeto do servidor do trimestre 3 aberto, com o terminal e o arquivo de ambiente na tela
  • Navegador com o painel do provedor de deploy aberto, em modo somente leitura

Conceitos

Senha e token não são o mesmo objeto

A diferença não é de nome. É de onde cada uma pode estar e de o que acontece quando vaza.

senhatoken
de quem éda pessoado dispositivo
onde viveno servidor, como hashno cliente e no servidor
pode ser lida de voltanãosim, e por isso é revogável
se vazartroca-se a senha de todo mundorevoga-se aquele token só
quem decide que valeo servidor, sempreo servidor, sempre

O token é revogável porque é um valor que o servidor guarda. Enquanto o valor estiver na lista, vale; quando sai da lista, não vale mais — e a placa, que tem o texto, não tem opinião. Essa é a assimetria que o aluno precisa internalizar: guardar a senha na placa é o que torna o problema insolúvel; guardar o token é o que torna o problema banal.

O outro lado da moeda é que o token não serve para nada sozinho. Ele não tem senha, não tem e-mail e não abre painel. Quem tem o token é a placa, e a placa faz só o que o firmware dela faz.

Onde o segredo pode existir: do GitHub à memória da placa

Um segredo em produção é um valor que algum servidor reconhece de verdade. Esse é o único critério: se o valor abre alguma coisa em algum lugar, ele é segredo em produção — mesmo que o lugar seja a rede do laboratório e o servidor seja o computador do professor. Um valor que só funciona no papel não vaza nada, porque não abre nada.

A tabela que o aluno leva da aula:

Onde o valor apareceQuem escreveO que o professor confere
sketch versionado no GitHubo aluno, na bancadanunca: aqui não morou segredo
variável de ambiente em produçãoquem opera o servidor, no deploysempre, e nunca no repositório
servidor de deployo painel do provedor ou o arquivo de ambiente da máquinasempre, e com permissão só do serviço
memória persistente da placao pareamento, no primeiro acessosempre, e nunca no sketch
Serial e logninguém, de propósitonunca

A variável de ambiente em produção é o lugar padrão de todo segredo do lado do servidor. O código lê process.env — ou os.environ, no Python — e o valor vem de fora. O repositório guarda o nome da variável e nada mais: um arquivo de exemplo com as linhas vazias já ensina quem vai configurar, e nenhuma delas precisa trazer o valor. A regra do projeto cabe numa frase: quem escreve no repositório escreve o nome da variável e o mecanismo, nunca o valor.

O segredo no servidor de deploy tem três origens possíveis, e o aluno precisa saber as três: o painel do provedor, que grava o valor no serviço e o entrega ao processo na hora do start; o arquivo de ambiente da máquina, que existe em disco e precisa de permissão só para o usuário do serviço; e o cofre do orquestrador, quando o serviço roda em container. Nenhuma delas é o repositório. Quando alguém escreve o valor direto no código do servidor, o problema deixa de ser o servidor e passa a ser o histórico do GitHub.

Provisionamento é o primeiro acesso, e ele vem de fora do sketch. A placa sai da fábrica sem nada: sem nome de rede, sem senha, sem token. Quem dá o primeiro dado é uma pessoa, na página de setup, ou é a fábrica, pela porta física. A consequência é o oposto de "coloquei no código": o segredo chega depois que o firmware já está gravado, por um caminho que não passa pelo arquivo.

É por isso que senha do WiFi em texto no firmware é o erro mais comum da semana e o mais fácil de não enxergar: ela funciona na bancada e funciona na rua, e só não funciona como segredo. O arquivo inteiro vai para um repositório que é espaço restrito com a credencial dentro, o que equivale a dizer que não é espaço restrito: qualquer pessoa com leitura no repositório lê a rede da escola.

Rota de fuga é o nome do caminho que o segredo percorre até chegar em alguém que vai usá-lo. Não é acaso, e por isso dá para fechar cada etapa:

Rota de fugaQuem lê sem pedirComo fechar
histórico do repositórioquem tem leitura no repositórioo valor nunca entra no arquivo; se já entrou, troque o valor no servidor e trate o histórico como leitura pública
arquivo de ambiente do servidor de deployquem entra na máquinaarquivo ignorado pelo Git, permissão só do usuário do serviço
linha de log com o cabeçalho completoquem tem leitura de logauth: [REDIGIDO] e nada mais
Serial da placaquem olha a bancada ou o vídeo da aulaimprimir tamanho e estado, nunca o valor
senha do WiFi no firmwarequem clona o repositórioprovisionamento, ou constante de exemplo com o comentário dizendo que é fictícia

Preferences: gravar no primeiro acesso, descartar no 401

A memória RAM da placa é apagada no instante em que falta alimentação. A memória não volátil mantém o conteúdo, e é onde o firmware, a configuração do WiFi e o token ficam gravados. Preferences é a interface para essa área, com três operações que resolvem a aula inteira:

Preferences memoria;
memoria.begin("dispositivo", false);
memoria.putString("tk", token);
memoria.end();

begin abre o espaço de nome, e false significa "abrindo para escrever". putString grava. end fecha e sincroniza: sem ele a gravação pode ficar pendente na cache, e o token some justamente na placa que foi desligada mal. A leitura é idêntica com true, e getString devolve um valor padrão quando a chave não existe.

O ponto que o professor precisa gritar: o token não está no sketch. Ele está num lugar da placa que se apaga com o git checkout e não vai para o repositório. Por isso o .ino pode ir para o GitHub inteiro sem vazar nada. Um segredo escrito no código é um segredo publicado, e o nome técnico disso é segredo no firmware.

Quando o servidor recusa o token, ele responde 401 Unauthorized com um corpo que costuma dizer algo como token revogado. O que a placa faz com isso é a aula inteira, e a resposta errada é quase sempre a mesma:

while (status != 201) {          // ERRADO
  enviarLeitura(token);
}

Esse while é um dispositivo que gasta bateria drenando a rede, e em cima disso nunca vai conseguir: o token é inválido, e não é uma rede instável. Não há retry que resolva, porque não houve falha temporária — houve uma decisão.

O tratamento certo tem três partes: parar, limpar o estado local e avisar. Limpar significa apagar o token da memória, para que a placa pare de tentar e para que o próximo estado não seja um estado com credencial morta. Avisar significa colocar algo no log, porque quem resolve isso é uma pessoa.

E por isso que o token vira um estado nomeado e não uma variável solta: com SEM_TOKEN, TOKEN_VALIDO, TOKEN_RECEBIDO e TOKEN_REVOGADO, o professor aponta a linha do código que decide o comportamento, e o aluno imprime o estado em vez de tentar adivinhar.

HTTPS resolve o caminho, e o resto continua aberto

HTTP sem criptografia é o estado padrão de um servidor de projeto: o token viaja no cabeçalho, em texto que qualquer pessoa no caminho da rede lê sem esforço — inclusive o roteador do laboratório, um ponto de acesso falso e o provedor. Cifrar o transporte é o que fecha essa porta, e o mecanismo é o TLS: as duas pontas acordam uma chave de sessão e o tráfego inteiro passa cifrado por ela. O nome HTTPS é só o HTTP dentro desse túnel; o que protege é a sessão, não o esquema do endereço.

O que o TLS resolve: a leitura por terceiro no caminho. O que ele não resolve: um token vazado continua valendo até ser revogado, e um servidor com https:// errado continua sendo um servidor.

O detalhe que separa cifrar de fingir que cifrou é o certificado. É ele que diz à placa para quem ela está falando, e é a conferência dele que impede que um intermediário finja ser o seu servidor. Uma placa que desliga a validação do certificado tem um túnel honesto aberto para o lugar errado.

Na placa, NetworkClientSecure é o objeto que faz isso, com um custo que o aluno precisa conhecer: a conexão cifrada leva vários segundos para subir numa placa de 240 MHz, e precisa de memória para o buffer. A escolha entre uma conexão nova por envio e uma conexão mantida aberta é o tradeoff real, e ele reaparece no dia 11.

Depois que o caminho está cifrado, sobra tudo o que o túnel não alcança. E o que mais sobra é o log.

Uma linha de log com IP do solicitante é um dado pessoal identificável, e a LGPD trata dele como trata de qualquer outro: ele tem finalidade declarada, prazo de guarda e alguém responsável. Guardar o endereço de quem chamou a API "porque sempre teve assim" não tem finalidade nenhuma, e é o registro mais fácil de eliminar sem quebrar o produto. A mesma linha costuma trazer o dado pessoal do usuário — nome, e-mail, endereço, o valor da leitura — e cada campo tem o mesmo tratamento.

A prática que fecha a aula: log de entrada, log de saída, log de erro, e nenhuma credencial, nenhum endereço de origem e nenhum campo identificável entre eles. Onde o log precisa falar do token, ele fala de uma forma só: auth: [REDIGIDO]. Onde precisa falar de quem chamou, ele fala do que a chamada fez, não de quem foi.

E vale para o Serial da placa: a aula de hoje não imprime o valor do token, nem em bancada, porque o hábito que se cria no laboratório é o hábito que vai para o produto. Privacidade não é uma etapa do projeto, é o formato padrão do registro. E a placa fica na casa de quem comprou: ela passa o dia inteiro vendo temperatura, horário e uso.

Atividade

Montagem:

Esta aula não pede circuito: o sketch não usa nenhum GPIO, e o instrumento da atividade é o monitor serial. O professor deixa a placa ligada pelo cabo USB e lê o que ela escreve, que é a mesma coisa que o Serial mostra.

  1. Grave o sketch da resolução e rode no monitor serial a 115200. Identifique, no log, o número do firmware e o estado da placa em cada bloco [estado].
  2. Desligue a placa do cabo, volte a ligar e rode de novo. O que aconteceu com o token? Onde ele estava guardado?
  3. Troque TOKEN_DE_EXEMPLO por qualquer outro valor de exemplo, grave, e veja que o token do setup e o token lido no boot são o mesmo. Explique o que aconteceria se a placa lesse o token de uma variável comum.
  4. Ligue REVOGADO_PARA_DEMO para true antes do primeiro envio e grave. O que o serial mostra no [envio 1]? Qual é a diferença de comportamento entre tentar de novo e limpar o estado?
  5. No servidor do projeto, procure no log qualquer linha com o token do dispositivo. Se houver, apague a linha e escreva no caderno qual foi a origem dela: log de requisição, console.log de depuração ou resposta de erro.
  6. Escreva, em quatro linhas, o que a placa faz em cada um destes casos: 201 recebido, 401 recebido, e placa sem rede depois de gravar a leitura no buffer.
  7. Abra no computador o arquivo de ambiente do servidor do projeto e liste, por linha, o nome de cada variável de ambiente em produção que o servidor usa. Coloque um X na que já tem valor preenchido. Nenhum valor entra no caderno: só o nome, e onde ele está gravado hoje.
  8. No repositório do projeto, procure a senha do WiFi e o token em todo lugar, inclusive no histórico de commits, e escreva o que apareceu. Diga, para cada achado, em qual etapa da rota de fuga ele está.
  9. Escolha uma linha de log real do projeto, corte o cabeçalho Authorization e o endereço do solicitante, e escreva a linha final no caderno. Diga o que você perdeu de diagnosticar e o que não pode perder.
  10. Escreva o caminho do segredo do dispositivo, do servidor de deploy até a memória da placa, marcando cada salto e dizendo em qual deles o valor aparece em texto claro.

Nota: 12 pontos. Critério de fim: o serial mostra os quatro estados, o professor viu o 401 com a placa limpando o token em vez de reenviar, e o caderno tem o caminho do segredo desenhado sem nenhum valor de verdade escrito nele.

Resolucao

// Aula 2 do dia 9: segredo, transporte e o dispositivo na rede.
//
// A placa guarda um TOKEN, nunca uma senha. A diferenca nao e vocabulario:
// senha e a credencial da pessoa, e ela nunca sai do servidor; token e a
// credencial da placa, e ela pode ser revogada sem tocar em senha nenhuma.
//
// O codigo tem um servidor de exemplo rodando DENTRO da propria placa. Ele
// responde 401 quando o token foi revogado, e o firmware tem que saber o que
// fazer com esse 401: repetir o envio seria o defeito.

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <NetworkClientSecure.h>
#include <Preferences.h>

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Transporte. HTTPS e o estado final do projeto: o token viaja no cabecalho
// de autenticacao, e em HTTP em claro ele viaja para qualquer pessoa na rede.
#define USAR_HTTPS false
#define URL_SERVIDOR "https://api.exemplo.invalido/v1/leitura"

// Nome do espaco de memoria persistente. A placa tem memoria nao volatil: o
// token continua valendo depois de desligar o cabo de alimentacao.
#define ESPACO_NVS "dispositivo"

// O token do dispositivo. Este valor e de EXEMPLO e nao vale em lugar nenhum:
// o token de verdade vem do servidor, na resposta do pareamento.
#define TOKEN_DE_EXEMPLO "tk-lab-bancada01"

// Chave sob a qual o token fica guardado na memoria persistente.
#define CHAVE_TOKEN "tk"

// Versao do firmware. Este e o numero que o professor le no serial para saber
// qual versao esta rodando depois de um deploy.
#define VERSAO_FIRMWARE "1.4.0"

Preferences memoria;

// Esta flag liga e desliga o "token revogado" do servidor de exemplo. E o que
// o professor alterna na frente da turma para mostrar o 401 chegando.
bool REVOGADO_PARA_DEMO = false;

// Os quatro estados que a placa pode estar. Uma string solta no meio do codigo
// e o que produz "token invalido" em um `if` e "token expirado" em outro: o
// estado precisa ter nome, porque o professor precisa conseguir apontar.
enum Estado {
  SEM_TOKEN,
  TOKEN_VALIDO,
  TOKEN_RECEBIDO,
  TOKEN_REVOGADO
};

Estado estadoAtual = SEM_TOKEN;

// ------------------------------------------------------------------ a placa

// Guarda o token na memoria persistente. Preferencias e o caminho certo: o
// token nao vai para o sketch, e por isso nao entra no git nem no log.
void guardarToken(const String& token) {
  memoria.begin(ESPACO_NVS, false);
  memoria.putString(CHAVE_TOKEN, token);
  memoria.end();
  Serial.println("[token] gravado na memoria persistente da placa");
}

// Le o token de volta. E o que faz a placa lembrar do dispositivo depois que
// alguem desligou o cabo de alimentacao.
String lerToken() {
  memoria.begin(ESPACO_NVS, true);
  String token = memoria.getString(CHAVE_TOKEN, "");
  memoria.end();
  return token;
}

// Apaga o token. E o que a placa faz quando recebe 401: ela nao pode se
// reautenticar sozinha, entao limpa o estado e espera o dono parear de novo.
void esquecerToken() {
  memoria.begin(ESPACO_NVS, false);
  memoria.remove(CHAVE_TOKEN);
  memoria.end();
  estadoAtual = SEM_TOKEN;
  Serial.println("[token] apagado da placa apos recusa do servidor");
}

String nomeDoEstado(Estado estado) {
  switch (estado) {
    case SEM_TOKEN:       return "SEM_TOKEN";
    case TOKEN_VALIDO:    return "TOKEN_VALIDO";
    case TOKEN_RECEBIDO:  return "TOKEN_RECEBIDO";
    default:              return "TOKEN_REVOGADO";
  }
}

// ------------------------------------------------- servidor de exemplo
//
// Roda dentro da propria placa para que a aula nao dependa de rede. Nao e um
// servidor de verdade: e o recorte do comportamento que o firmware precisa
// tratar. As tres respostas que importam sao 201, 401 e 503.

int responderServidorDeExemplo(bool tokenRevogado, bool servidorLento) {
  if (servidorLento) {
    return 503;
  }
  if (tokenRevogado) {
    return 401;
  }
  return 201;
}

// ------------------------------------------------------------------ envio

// Envia uma leitura e devolve o codigo de status. Separar "enviar" de
// "decidir o que fazer com a resposta" e o que permite testar os dois.
int enviarLeitura(const String& token) {
  Serial.println("[http] abrindo conexao " + String(USAR_HTTPS ? "TLS" : "em claro"));
  if (USAR_HTTPS) {
    Serial.println("[tls] sem certificado validado: aceitavel so em laboratorio,");
    Serial.println("[tls] em producao a placa precisa conferir o certificado do servidor.");
  } else {
    Serial.println("[http] token vai no cabecalho em texto legivel. Nao use assim.");
  }

  int status = responderServidorDeExemplo(REVOGADO_PARA_DEMO, false);
  Serial.println("[http] resposta: " + String(status));
  return status;
}

// ------------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println();
  Serial.println("=== dia 9, aula 2: segredo, transporte e dispositivo ===");
  Serial.println("firmware: " + String(VERSAO_FIRMWARE));
  Serial.println();

  Serial.println("[placa] token em memoria volatil: NAO. Va para Preferences.");
  Serial.println("[placa] senha da pessoa: NUNCA entra aqui. Isso nao existe na placa.");
  Serial.println();

  // 1. O pareamento: o servidor entrega o token, a placa guarda.
  Serial.println("[pareamento] token de exemplo recebido do servidor:");
  Serial.println("[pareamento]   " + String(TOKEN_DE_EXEMPLO));
  guardarToken(TOKEN_DE_EXEMPLO);
  estadoAtual = TOKEN_RECEBIDO;
  Serial.println("[estado] " + nomeDoEstado(estadoAtual));
  Serial.println();

  // 2. O token sobrevive ao desligamento.
  String tokenRecuperado = lerToken();
  Serial.println("[boot] token lido da memoria persistente, " +
                 String(tokenRecuperado.length()) + " bytes: " +
                 String(tokenRecuperado == TOKEN_DE_EXEMPLO ? "presente" : "ausente"));
  Serial.println("[estado] " + nomeDoEstado(estadoAtual));
  Serial.println();

  // 3. Envio normal.
  Serial.println("[envio 1] token aceito pelo servidor:");
  int status = enviarLeitura(tokenRecuperado);
  if (status == 201) {
    estadoAtual = TOKEN_VALIDO;
    Serial.println("[decisao] 201: gravado. O token serve.");
  }
  Serial.println("[estado] " + nomeDoEstado(estadoAtual));
  Serial.println();

  // 4. O servidor revoga o token. A placa ainda tem o valor na mao.
  Serial.println("[servidor] dono da conta revoga o token no painel.");
  Serial.println("[servidor] o valor continua gravado na placa. Isso e o ponto.");
  REVOGADO_PARA_DEMO = true;
  Serial.println();

  // 5. A placa tenta de novo e leva 401.
  Serial.println("[envio 2] a placa reenvia com o mesmo token:");
  status = enviarLeitura(tokenRecuperado);
  if (status == 401) {
    Serial.println("[decisao] 401: token recusado. Nao reenvio, nao espero, nao insisto.");
    esquecerToken();
  } else if (status == 201) {
    Serial.println("[decisao] o token revogado ainda funcionou. Isso e um defeito do servidor.");
  }
  Serial.println("[estado] " + nomeDoEstado(estadoAtual));
  Serial.println();

  // 6. O que a placa faz depois: fica parada e avisa.
  Serial.println("[recuperacao] sem token, a placa so faz duas coisas:");
  Serial.println("[recuperacao]   1. avisar no display que precisa de pareamento");
  Serial.println("[recuperacao]   2. tentar parear quando alguem abrir a pagina de setup");
  Serial.println("[recuperacao] ela nao tenta adivinhar a senha. Ela nunca a viu.");
  Serial.println();

  // 7. O que o professor precisa ver quando o log vaza.
  Serial.println("[log] a linha de log NUNCA leva o token:");
  Serial.println("[log]   leitura recebida de esp32-bancada-01");
  Serial.println("[log]   auth: [REDIGIDO] status=201");
  Serial.println("[log] levar o token inteiro no log e entregar a credencial do dispositivo.");
  Serial.println();
  Serial.println("[placa] heap livre: " + String(ESP.getFreeHeap()) + " bytes");
  Serial.println("[fim]");
}

void loop() {
  delay(8000);
  Serial.println("[placa] viva | estado: " + nomeDoEstado(estadoAtual) +
                 " | heap: " + String(ESP.getFreeHeap()) + " bytes");
}

Por que assim e não de outro jeito. A enum Estado com quatro estados é a decisão de design da aula. A alternativa — um if (token == "") espalhado por cinco lugares — funciona igual e é ilegível na semana seguinte. Com estado nomeado, o Serial.println do loop imprime o que a placa está fazendo, e o professor lê de longe.

A função enviarLeitura não faz HTTP de verdade: ela chama responderServidorDeExemplo, que devolve o código de status. Isso é deliberado, pelo mesmo motivo da aula 1 — uma aula que depende de rede ao vivo perde metade da turma no primeiro timeout. O que o sketch ensina é o if que vem depois da chamada, e esse if é o objeto da aula.

O REVOGADO_PARA_DEMO fica como flag global e não como parâmetro porque a aula precisa de dois estados no mesmo setup: enviar com o token aceito, depois revogar, depois enviar de novo. Com parâmetro, seria preciso reescrever a ordem do roteiro.

O USAR_HTTPS fica false de propósito, com o comentário explicando que token em HTTP em claro é defeito. O professor liga para true na frente da turma e a placa demora vários segundos a mais na conexão — esse atraso é a aula, e ele não existe num comentário.

O TOKEN_DE_EXEMPLO está no sketch, e ainda assim o sketch está certo: é um valor de demonstração, e o comentário diz isso na linha de cima. A distinção que o professor faz em voz alta é entre "segredo de exemplo, com valor inventado e comentado" e "segredo real, com valor que funciona em algum lugar". O segundo não entra no material nunca.

Criterios de correcao

CritérioPontos
Token guardado em Preferences e lido de volta no boot, com begin, putString e end na ordem3 pontos
401 tratado sem novo envio, com o token apagado e o estado virando SEM_TOKEN3 pontos
Item 7 entregue só com nome das variáveis de ambiente em produção, sem nenhum valor2 pontos
Serial sem valor de token, sem senha do WiFi e sem endereço de origem em nenhum bloco2 pontos
Item 9: linha reescrita sem cabeçalho Authorization, com o status preservado1 ponto
Item 10: todos os saltos do caminho escritos, e nenhum deles com o valor no sketch1 ponto

Erros comuns

ErroComo apareceCorreção
Token guardado em variável comumO token funciona até a próxima reinicialização"RAM é apagada no desligamento. Preferences grava em memória não volátil e o dispositivo sobrevive ao corte de energia."
begin sem endToken às vezes some, "às vezes""O end sincroniza a cache. Sem ele a gravação fica pendente e o token se perde justamente quando a placa foi desligada mal."
Reenviar em laço até dar certowhile (status != 201) enviarLeitura();"Token recusado não é falha temporária. Não há espera que resolva. Pare, limpe o estado, avise a pessoa."
Imprimir o token para "verificar se gravou"[token] gravado: tk-lab-bancada01"Verifique o tamanho e o estado, nunca o valor. É um hábito: o que você imprime no laboratório, você imprime no produto."
Senha do WiFi no topo do sketch#define WIFI_PASSWORD "..." sem nenhuma nota"O valor é fictício e comentado como tal. Sem o comentário, ninguém sabe se aquilo é exemplo ou é a rede da escola."
git add . na pasta do servidor, com o arquivo de ambiente juntoArquivo de ambiente versionado no GitHub"Ignore o arquivo de ambiente antes do primeiro add e versione só o arquivo de exemplo, com os nomes das variáveis e nenhum valor."
Achar que o repositório é espaço restrito"Só a equipe tem acesso, então está protegido""Restringir quem entra não protege o arquivo: protege quem chega depois do vazamento. O segredo não entra no repositório."
Contar o repositório como cofre da placa"A placa tem memória protegida, então o token está seguro""A memória é espaço restrito do firmware, não do operador. Quem tem a placa e o valor de pareamento lê o token."
HTTPS como configuração de uma flagAluno troca https:// e acha que resolveu"Trocar o esquema é papel. O que protege é a sessão inteira e o certificado que a placa precisa conferir. São coisas diferentes."
Log com o endereço do solicitante "porque sempre teve assim"req de 203.0.113.7 status=201"Endereço de origem é dado pessoal e não tem finalidade declarada. A LGPD trata igual: quem registra, por quê e até quando."
Apagar o log inteiro em vez da linha"rm log.txt""Apague a linha com a credencial. O resto do log é a única forma de achar o erro amanhã."

Desafio extra

Implemente a página de pareamento: quando a placa está em SEM_TOKEN, ela sobe um WebServer em 192.168.4.1 e mostra um código de seis dígitos. O professor, em outro celular, conecta nessa rede, digita o código numa rota do servidor, e o servidor devolve o token. Meça quanto tempo leva do SEM_TOKEN até o TOKEN_VALIDO e escreva por que esse tempo é o que a pessoa vive como "o dispositivo não funciona". Depois some a rota de devolução do token do log do servidor e escreva o que um técnico deixa de conseguir diagnosticar quando isso acontece.

>

A resolucao, compilada

// Aula 2 do dia 9: segredo, transporte e o dispositivo na rede.
//
// A placa guarda um TOKEN, nunca uma senha. A diferenca nao e vocabulario:
// senha e a credencial da pessoa, e ela nunca sai do servidor; token e a
// credencial da placa, e ela pode ser revogada sem tocar em senha nenhuma.
//
// O codigo tem um servidor de exemplo rodando DENTRO da propria placa. Ele
// responde 401 quando o token foi revogado, e o firmware tem que saber o que
// fazer com esse 401: repetir o envio seria o defeito.

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <NetworkClientSecure.h>
#include <Preferences.h>

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Transporte. HTTPS e o estado final do projeto: o token viaja no cabecalho
// de autenticacao, e em HTTP em claro ele viaja para qualquer pessoa na rede.
#define USAR_HTTPS false
#define URL_SERVIDOR "https://api.exemplo.invalido/v1/leitura"

// Nome do espaco de memoria persistente. A placa tem memoria nao volatil: o
// token continua valendo depois de desligar o cabo de alimentacao.
#define ESPACO_NVS "dispositivo"

// O token do dispositivo. Este valor e de EXEMPLO e nao vale em lugar nenhum:
// o token de verdade vem do servidor, na resposta do pareamento.
#define TOKEN_DE_EXEMPLO "tk-lab-bancada01"

// Chave sob a qual o token fica guardado na memoria persistente.
#define CHAVE_TOKEN "tk"

// Versao do firmware. Este e o numero que o professor le no serial para saber
// qual versao esta rodando depois de um deploy.
#define VERSAO_FIRMWARE "1.4.0"

Preferences memoria;

// Esta flag liga e desliga o "token revogado" do servidor de exemplo. E o que
// o professor alterna na frente da turma para mostrar o 401 chegando.
bool REVOGADO_PARA_DEMO = false;

// Os quatro estados que a placa pode estar. Uma string solta no meio do codigo
// e o que produz "token invalido" em um `if` e "token expirado" em outro: o
// estado precisa ter nome, porque o professor precisa conseguir apontar.
enum Estado {
  SEM_TOKEN,
  TOKEN_VALIDO,
  TOKEN_RECEBIDO,
  TOKEN_REVOGADO
};

Estado estadoAtual = SEM_TOKEN;

// ------------------------------------------------------------------ a placa

// Guarda o token na memoria persistente. Preferencias e o caminho certo: o
// token nao vai para o sketch, e por isso nao entra no git nem no log.
void guardarToken(const String& token) {
  memoria.begin(ESPACO_NVS, false);
  memoria.putString(CHAVE_TOKEN, token);
  memoria.end();
  Serial.println("[token] gravado na memoria persistente da placa");
}

// Le o token de volta. E o que faz a placa lembrar do dispositivo depois que
// alguem desligou o cabo de alimentacao.
String lerToken() {
  memoria.begin(ESPACO_NVS, true);
  String token = memoria.getString(CHAVE_TOKEN, "");
  memoria.end();
  return token;
}

// Apaga o token. E o que a placa faz quando recebe 401: ela nao pode se
// reautenticar sozinha, entao limpa o estado e espera o dono parear de novo.
void esquecerToken() {
  memoria.begin(ESPACO_NVS, false);
  memoria.remove(CHAVE_TOKEN);
  memoria.end();
  estadoAtual = SEM_TOKEN;
  Serial.println("[token] apagado da placa apos recusa do servidor");
}

String nomeDoEstado(Estado estado) {
  switch (estado) {
    case SEM_TOKEN:       return "SEM_TOKEN";
    case TOKEN_VALIDO:    return "TOKEN_VALIDO";
    case TOKEN_RECEBIDO:  return "TOKEN_RECEBIDO";
    default:              return "TOKEN_REVOGADO";
  }
}

// ------------------------------------------------- servidor de exemplo
//
// Roda dentro da propria placa para que a aula nao dependa de rede. Nao e um
// servidor de verdade: e o recorte do comportamento que o firmware precisa
// tratar. As tres respostas que importam sao 201, 401 e 503.

int responderServidorDeExemplo(bool tokenRevogado, bool servidorLento) {
  if (servidorLento) {
    return 503;
  }
  if (tokenRevogado) {
    return 401;
  }
  return 201;
}

// ------------------------------------------------------------------ envio

// Envia uma leitura e devolve o codigo de status. Separar "enviar" de
// "decidir o que fazer com a resposta" e o que permite testar os dois.
int enviarLeitura(const String& token) {
  Serial.println("[http] abrindo conexao " + String(USAR_HTTPS ? "TLS" : "em claro"));
  if (USAR_HTTPS) {
    Serial.println("[tls] sem certificado validado: aceitavel so em laboratorio,");
    Serial.println("[tls] em producao a placa precisa conferir o certificado do servidor.");
  } else {
    Serial.println("[http] token vai no cabecalho em texto legivel. Nao use assim.");
  }

  int status = responderServidorDeExemplo(REVOGADO_PARA_DEMO, false);
  Serial.println("[http] resposta: " + String(status));
  return status;
}

// ------------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println();
  Serial.println("=== dia 9, aula 2: segredo, transporte e dispositivo ===");
  Serial.println("firmware: " + String(VERSAO_FIRMWARE));
  Serial.println();

  Serial.println("[placa] token em memoria volatil: NAO. Va para Preferences.");
  Serial.println("[placa] senha da pessoa: NUNCA entra aqui. Isso nao existe na placa.");
  Serial.println();

  // 1. O pareamento: o servidor entrega o token, a placa guarda.
  Serial.println("[pareamento] token de exemplo recebido do servidor:");
  Serial.println("[pareamento]   " + String(TOKEN_DE_EXEMPLO));
  guardarToken(TOKEN_DE_EXEMPLO);
  estadoAtual = TOKEN_RECEBIDO;
  Serial.println("[estado] " + nomeDoEstado(estadoAtual));
  Serial.println();

  // 2. O token sobrevive ao desligamento.
  String tokenRecuperado = lerToken();
  Serial.println("[boot] token lido da memoria persistente, " +
                 String(tokenRecuperado.length()) + " bytes: " +
                 String(tokenRecuperado == TOKEN_DE_EXEMPLO ? "presente" : "ausente"));
  Serial.println("[estado] " + nomeDoEstado(estadoAtual));
  Serial.println();

  // 3. Envio normal.
  Serial.println("[envio 1] token aceito pelo servidor:");
  int status = enviarLeitura(tokenRecuperado);
  if (status == 201) {
    estadoAtual = TOKEN_VALIDO;
    Serial.println("[decisao] 201: gravado. O token serve.");
  }
  Serial.println("[estado] " + nomeDoEstado(estadoAtual));
  Serial.println();

  // 4. O servidor revoga o token. A placa ainda tem o valor na mao.
  Serial.println("[servidor] dono da conta revoga o token no painel.");
  Serial.println("[servidor] o valor continua gravado na placa. Isso e o ponto.");
  REVOGADO_PARA_DEMO = true;
  Serial.println();

  // 5. A placa tenta de novo e leva 401.
  Serial.println("[envio 2] a placa reenvia com o mesmo token:");
  status = enviarLeitura(tokenRecuperado);
  if (status == 401) {
    Serial.println("[decisao] 401: token recusado. Nao reenvio, nao espero, nao insisto.");
    esquecerToken();
  } else if (status == 201) {
    Serial.println("[decisao] o token revogado ainda funcionou. Isso e um defeito do servidor.");
  }
  Serial.println("[estado] " + nomeDoEstado(estadoAtual));
  Serial.println();

  // 6. O que a placa faz depois: fica parada e avisa.
  Serial.println("[recuperacao] sem token, a placa so faz duas coisas:");
  Serial.println("[recuperacao]   1. avisar no display que precisa de pareamento");
  Serial.println("[recuperacao]   2. tentar parear quando alguem abrir a pagina de setup");
  Serial.println("[recuperacao] ela nao tenta adivinhar a senha. Ela nunca a viu.");
  Serial.println();

  // 7. O que o professor precisa ver quando o log vaza.
  Serial.println("[log] a linha de log NUNCA leva o token:");
  Serial.println("[log]   leitura recebida de esp32-bancada-01");
  Serial.println("[log]   auth: [REDIGIDO] status=201");
  Serial.println("[log] levar o token inteiro no log e entregar a credencial do dispositivo.");
  Serial.println();
  Serial.println("[placa] heap livre: " + String(ESP.getFreeHeap()) + " bytes");
  Serial.println("[fim]");
}

void loop() {
  delay(8000);
  Serial.println("[placa] viva | estado: " + nomeDoEstado(estadoAtual) +
                 " | heap: " + String(ESP.getFreeHeap()) + " bytes");
}

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