Volver al blog
SeguridadIoTBun

Neveras hackeadas en Europa: seguridad IoT con Bun

13 de octubre de 2026·7 min de lectura·Diego Horvatti

Unos hackers entraron en supermercados de Europa y activaron el descongelamiento automático de las neveras. No robaron datos de tarjetas ni pidieron rescate en la pantalla de la caja. Mandaron a las neveras a hacer algo que ya saben hacer. La historia es un gran caso para hablar de seguridad IoT, y de cómo tú, que escribes backend en TypeScript, deberías pensar cualquier sistema que manda comandos al mundo físico.

La noticia salió en Tom's Hardware. Según el reportaje, el ataque también afectó las entregas y los precios en las tiendas.

Qué pasó con las neveras de los supermercados

Un supermercado grande no controla la refrigeración con un termostato en la pared. Las vitrinas frías, las cámaras y los congeladores están conectados a controladores. Esos controladores hablan con un sistema central, que puede ser del propio minorista o de un proveedor que atiende a varias cadenas.

Desde ese sistema central, un técnico ve la temperatura de cada equipo, ajusta el setpoint y programa el descongelamiento. Descongelar es rutina. El serpentín del evaporador acumula hielo, pierde eficiencia, y el sistema se calienta un poco durante unos minutos para derretirlo todo. Pasa varias veces al día en cualquier tienda.

Los atacantes usaron justamente esa función. No voy a especular sobre el vector exacto de entrada. Para lo que importa aquí, basta con el esquema general: alguien consiguió acceso a un panel que manda comandos a muchas tiendas y disparó un comando legítimo en el momento equivocado, a escala.

Si los precios también se vieron afectados, tiene sentido pensar que estaban en el mismo ecosistema de gestión de tienda. Eso vale para el TPV, para el ERP o para las etiquetas electrónicas de los estantes. Cuando todo pasa por un solo lugar, un acceso indebido lo tumba todo de una vez.

Por qué un descongelamiento se convierte en pérdidas

Un descongelamiento programado dura poco y está calculado para que la temperatura del producto no salga del rango seguro. Un descongelamiento forzado, prolongado o repetido es otra historia.

Piensa en un congelador de helados. Necesita estar cerca de -18 °C. Si se calienta y luego se vuelve a congelar, el producto puede parecer normal, pero la textura ya no sirve y la norma sanitaria obliga a desecharlo. Con carne, pollo y lácteos la lógica es la misma: si pasó demasiado tiempo por encima de unos 5 °C, va a la basura. Nadie se arriesga a intoxicar a un cliente para salvar el stock.

Ahora multiplícalo por cientos de tiendas. Es pérdida de producto, horas de personal revisando temperaturas con el termómetro en la mano y estantes vacíos. Súmale entregas atrasadas y precios equivocados en las góndolas. Nadie tuvo que cifrar un solo archivo para causar un daño real.

En alguna tienda por ahí, el helado se convirtió en batido a las tres de la mañana sin que nadie lo pidiera.

Seguridad IoT: el comando legítimo es el arma

Aquí es donde el caso se pone interesante para quien programa. Solemos pensar en un ataque como algo raro: un payload malformado, SQL injection, un exploit de memoria. Aquí el comando era perfectamente válido. El sistema hizo exactamente lo que le mandaron.

El comando más peligroso de tu sistema es el que fue hecho para obedecer.

Eso cambia la pregunta. Validar que la petición esté bien formada no basta. Tienes que preguntarte si tiene sentido ese comando, de ese usuario, en esa cantidad, a esa hora.

Y hay un detalle que poca gente enfrenta: si el atacante controla el panel central, tiene las credenciales del panel central. Firma válida, token válido, sesión válida. Toda la autenticación va a decir "adelante". La defensa que queda es limitar lo que un acceso legítimo puede hacer de una sola vez.

Cómo protegería yo los comandos remotos con Bun

Supongamos un gateway en cada tienda, corriendo Bun, que recibe comandos de la central y los reenvía a los controladores. No es el único diseño posible, pero es común y sirve bien de ejemplo.

El primer paso es lo básico bien hecho: comando firmado, lista cerrada de tipos permitidos y validez corta para no aceptar 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("firma inválida", { status: 401 });
    }

    const cmd = JSON.parse(body);
    if (!ALLOWED.has(cmd.type)) return new Response("comando desconocido", { status: 400 });
    if (Date.now() - cmd.issuedAt > 30_000) return new Response("comando expirado", { status: 400 });

    // reenvía al controlador...
    return Response.json({ ok: true });
  },
});

Nada de esto es nuevo. Bun corre node:crypto sin problemas, y timingSafeEqual evita filtrar la firma por el tiempo de respuesta. Pero, como dije, esto solo frena a quien no tiene la clave. Contra un panel comprometido, no ayuda en nada.

¿Cómo limitar el daño de un comando masivo?

La parte que realmente habría marcado la diferencia es el freno. Un contador simple que diga: "¿más de X descongelamientos manuales en Y minutos? Para todo y llama a un humano". Con bun:sqlite, esto cabe en pocas líneas y además deja un rastro de auditoría.

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; // calibra según el historial de la tienda

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;
}

Tres detalles importan más que el código:

  • El límite vive en la tienda, no en la central. Si vive en el mismo servidor que fue invadido, el atacante desactiva el límite antes de actuar.
  • El número sale del historial. Revisa los logs de seis meses y descubre cuántos descongelamientos manuales recibe una tienda en un día normal. Si la respuesta es "casi ninguno", tres en diez minutos ya es mucho.
  • Los intentos denegados también se registran. Una ráfaga de bloqueos a las tres de la mañana es tu alarma de incendios.

Y la última línea de defensa ni siquiera debería ser software de servidor. Debería estar en el propio controlador: el descongelamiento se aborta solo si la temperatura del producto supera un límite fijo, sin importar quién lo mandó. Eso es firmware y física, y ningún panel remoto debería poder sobrescribirlo.

Qué cambia en tu flujo de trabajo

Quizás nunca escribas software para neveras. Pero casi todo dev ya hizo un endpoint que actúa en lote: reprocesar pedidos, reenviar un correo a toda la base, actualizar el precio de todos los productos, desactivar usuarios por filtro. Es la misma familia de problemas.

Algunas preguntas que empecé a hacer en las revisiones de código:

  • Si esta credencial se filtra, ¿cuál es el peor comando que puede disparar?
  • ¿Existe un tope de cuántos registros, dispositivos o tiendas afecta una sola llamada?
  • ¿Una operación masiva pide que una segunda persona la apruebe?
  • ¿El log de quién mandó qué está en un lugar que el propio sistema no puede borrar?

Ninguna de estas respuestas exige un framework nuevo. Es una tabla, un contador y un if. Lo difícil es acordarse de ponerlo antes del incidente, y no en el post-mortem.

Mi opinión

Un botón que actúa en mil tiendas a la vez, sin límite y sin segunda confirmación, es un bug, no una feature. Aprendimos a proteger datos. Todavía estamos aprendiendo a proteger acciones, sobre todo cuando la acción sale del servidor y calienta un congelador de verdad.

Bun no salva a nadie por sí solo. Lo que hace bien es quitarte la excusa: servidor HTTP, SQLite y criptografía ya vienen incluidos, así que poner un freno decente cuesta una tarde. Si quieres ver cómo aplico este tipo de cuidado en sistemas reales, échale un vistazo a los proyectos que mantengo.

Resumen para LinkedIn

Unos hackers entraron en supermercados de Europa y no robaron ningún dato. Solo mandaron a las neveras a descongelarse.

El comando era válido. El sistema obedeció, en cientos de tiendas a la vez, y el helado se convirtió en batido a las tres de la mañana.

El comando más peligroso de tu sistema es el que fue hecho para obedecer.

Si cae el panel central, una firma y un token válidos no detienen a nadie. Lo que te salva es un freno local: un límite de acciones por ventana de tiempo, un log que el sistema no puede borrar y un bloqueo físico en el controlador.

Con Bun y bun:sqlite, ese freno se hace en una tarde. Y sirve para cualquier endpoint que actúa en lote, no solo para neveras.

Escribí el caso completo con código en el blog. ¿Cuál es el peor comando que una credencial filtrada puede disparar en tu sistema?

#SeguridadInformática #IoT #Bun #TypeScript #Backend