Geladeiras hackeadas na Europa: segurança IoT com Bun
Hackers invadiram supermercados na Europa e ativaram o degelo automático das geladeiras. Não roubaram dados de cartão nem pediram resgate pela tela do caixa. Mandaram as geladeiras fazerem uma coisa que elas já sabem fazer. A história é um ótimo caso para falar de segurança IoT, e de como você, que escreve backend em TypeScript, deveria pensar qualquer sistema que manda comando para o mundo físico.
A notícia saiu no Tom's Hardware. Segundo a reportagem, o ataque também afetou entregas e os preços nas lojas.
O que aconteceu com as geladeiras dos supermercados
Supermercado grande não controla refrigeração com um termostato na parede. Os balcões frios, as câmaras e os freezers ficam ligados a controladores. Esses controladores falam com um sistema central, que pode ser do próprio varejista ou de um fornecedor que atende várias redes.
Por esse sistema central, um técnico vê a temperatura de cada equipamento, ajusta o setpoint e agenda o degelo. Degelo é rotina. A serpentina do evaporador acumula gelo, perde eficiência, e o sistema esquenta um pouco por alguns minutos para derreter tudo. Acontece várias vezes por dia em qualquer loja.
Os atacantes usaram justamente essa função. Não vou especular sobre o vetor exato de entrada. Para o que interessa aqui, basta o desenho geral: alguém conseguiu acesso a um painel que manda comandos para muitas lojas e disparou um comando legítimo na hora errada, em escala.
Se os preços também foram afetados, faz sentido imaginar que estavam no mesmo ecossistema de gestão de loja. Isso vale para o PDV, para o ERP ou para as etiquetas eletrônicas de gôndola. Quando tudo passa por um lugar só, um acesso indevido derruba tudo de uma vez.
Por que um degelo vira prejuízo
Um degelo programado dura pouco e é calculado para a temperatura do produto não sair da faixa segura. Um degelo forçado, prolongado ou repetido é outra conversa.
Pense num freezer de sorvete. Ele precisa ficar perto de -18 °C. Se esquentar e depois congelar de novo, o produto até parece normal, mas a textura já era e a regra sanitária manda descartar. Com carne, frango e laticínios a lógica é a mesma: passou tempo demais acima de uns 5 °C, vai para o lixo. Ninguém arrisca intoxicar cliente para salvar o estoque.
Agora multiplique por centenas de lojas. É perda de produto, horas de equipe conferindo temperatura com termômetro na mão e gôndola vazia. Some entrega atrasada e preço errado na prateleira. Ninguém precisou criptografar um único arquivo para causar estrago de verdade.
Em alguma loja por aí, o sorvete virou milk-shake às três da manhã sem ninguém pedir.
Segurança IoT: o comando legítimo é a arma
É aqui que o caso fica interessante para quem programa. A gente costuma pensar ataque como algo esquisito: payload malformado, SQL injection, exploit de memória. Aqui o comando era perfeitamente válido. O sistema fez exatamente o que foi mandado.
O comando mais perigoso do seu sistema é aquele que ele foi feito para obedecer.
Isso muda a pergunta. Validar se a requisição é bem formada não basta. Você precisa perguntar se faz sentido esse comando, vindo desse usuário, nessa quantidade, neste horário.
E tem um detalhe que pouca gente encara: se o atacante domina o painel central, ele tem as credenciais do painel central. Assinatura válida, token válido, sessão válida. Toda a autenticação vai dizer "pode passar". A defesa que sobra é limitar o que um acesso legítimo consegue fazer de uma vez só.
Como eu protegeria comandos remotos com Bun
Vamos supor um gateway em cada loja, rodando Bun, que recebe comandos da central e repassa para os controladores. Não é o único desenho possível, mas é comum e serve bem de exemplo.
O primeiro passo é o básico bem feito: comando assinado, lista fechada de tipos permitidos e validade curta para não aceitar replay.
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = Bun.env.COMMAND_SECRET!;
const ALLOWED = new Set(["read_temp", "set_setpoint", "defrost"]);
function verify(body: string, signature: string) {
const expected = createHmac("sha256", SECRET).update(body).digest();
const received = Buffer.from(signature, "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
}
Bun.serve({
port: 3000,
async fetch(req) {
const body = await req.text();
if (!verify(body, req.headers.get("x-signature") ?? "")) {
return new Response("assinatura inválida", { status: 401 });
}
const cmd = JSON.parse(body);
if (!ALLOWED.has(cmd.type)) return new Response("comando desconhecido", { status: 400 });
if (Date.now() - cmd.issuedAt > 30_000) return new Response("comando expirado", { status: 400 });
// repassa para o controlador...
return Response.json({ ok: true });
},
});
Nada disso é novidade. O Bun roda node:crypto sem drama, e o timingSafeEqual evita vazar a assinatura pelo tempo de resposta. Mas, como eu disse, isso só segura quem não tem a chave. Contra um painel comprometido, não ajuda em nada.
Como limitar o estrago de um comando em massa?
A parte que realmente teria feito diferença é o freio. Um contador simples que diz: "mais de X degelos manuais em Y minutos? Para tudo e chama um humano". Com o bun:sqlite, isso cabe em poucas linhas e ainda deixa trilha de auditoria.
import { Database } from "bun:sqlite";
const db = new Database("audit.sqlite");
db.run("CREATE TABLE IF NOT EXISTS commands (ts INTEGER, actor TEXT, type TEXT, device TEXT, allowed INTEGER)");
const WINDOW_MS = 10 * 60_000;
const MAX_MANUAL_DEFROST = 3; // calibre pelo histórico da loja
export function allowDefrost(actor: string, device: string) {
const { n } = db
.query("SELECT count(*) AS n FROM commands WHERE type = 'defrost' AND allowed = 1 AND ts > ?")
.get(Date.now() - WINDOW_MS) as { n: number };
const ok = n < MAX_MANUAL_DEFROST;
db.query("INSERT INTO commands VALUES (?, ?, 'defrost', ?, ?)").run(Date.now(), actor, device, ok ? 1 : 0);
return ok;
}
Três detalhes importam mais que o código:
- O limite mora na loja, não na central. Se ele morar no mesmo servidor que foi invadido, o atacante desliga o limite antes de agir.
- O número vem do histórico. Olhe os logs de seis meses e descubra quantos degelos manuais uma loja recebe num dia normal. Se a resposta for "quase nenhum", três em dez minutos já é muito.
- Tentativa negada também é registrada. Uma rajada de bloqueios às três da manhã é o seu alarme de incêndio.
E a última linha de defesa nem deveria ser software de servidor. Deveria estar no próprio controlador: degelo aborta sozinho se a temperatura do produto passar de um limite fixo, não importa quem mandou. Isso é firmware e física, e nenhum painel remoto deveria conseguir sobrescrever.
O que muda no seu fluxo de trabalho
Você talvez nunca escreva software para geladeira. Mas quase todo dev já fez um endpoint que age em lote: reprocessar pedidos, reenviar e-mail para a base inteira, atualizar preço de todos os produtos, desativar usuários por filtro. É a mesma família de problema.
Algumas perguntas que eu passei a fazer em revisão de código:
- Se esta credencial vazar, qual é o pior comando que ela consegue disparar?
- Existe um teto de quantos registros, dispositivos ou lojas uma única chamada afeta?
- Operação em massa pede uma segunda pessoa para aprovar?
- O log de quem mandou o quê fica num lugar que o próprio sistema não consegue apagar?
Nenhuma dessas respostas exige framework novo. É uma tabela, um contador e um if. O difícil é lembrar de colocar antes do incidente, e não no post-mortem.
Minha opinião
Botão que age em mil lojas ao mesmo tempo, sem limite e sem segunda confirmação, é bug, não feature. A gente aprendeu a proteger dado. Ainda estamos aprendendo a proteger ação, principalmente quando a ação sai do servidor e esquenta um freezer de verdade.
O Bun não salva ninguém sozinho. O que ele faz bem é tirar a desculpa: servidor HTTP, SQLite e criptografia já vêm na caixa, então colocar um freio decente custa uma tarde. Se você quiser ver como eu aplico esse tipo de cuidado em sistemas reais, dá uma olhada nos projetos que eu mantenho.
Resumo para o LinkedIn
Hackers invadiram supermercados na Europa e não roubaram nenhum dado. Só mandaram as geladeiras fazerem degelo. O comando era válido. O sistema obedeceu, em centenas de lojas ao mesmo tempo, e o sorvete virou milk-shake às três da manhã. O comando mais perigoso do seu sistema é aquele que ele foi feito para obedecer. Se o painel central cai, assinatura e token válidos não seguram ninguém. O que salva é um freio local: limite de ações por janela de tempo, log que o sistema não consegue apagar e uma trava física no controlador. Com Bun e bun:sqlite, esse freio cabe numa tarde. E vale para qualquer endpoint que age em lote, não só para geladeira. Escrevi o caso completo com código no blog. Qual é o pior comando que uma credencial vazada dispara no seu sistema? #SegurançaDaInformação #IoT #Bun #TypeScript #Backend