Back to the blog
SecurityIoTBun

Hacked fridges in Europe: IoT security with Bun

October 13, 2026·7 min read·Diego Horvatti

Hackers broke into supermarkets in Europe and turned on the fridges' automatic defrost. They didn't steal card data. They didn't demand a ransom on the checkout screen. They told the fridges to do something they already know how to do. The story is a great case for talking about IoT security. It's also a good lesson for you, the TypeScript backend dev, on how to think about any system that sends commands to the physical world.

The news came out on Tom's Hardware. According to the report, the attack also hit deliveries and in-store prices.

What happened to the supermarket fridges

A big supermarket doesn't control refrigeration with a thermostat on the wall. The display cases, cold rooms and freezers are wired to controllers. Those controllers talk to a central system. It can belong to the retailer or to a vendor that serves several chains.

Through that central system, a technician sees the temperature of each unit, adjusts the setpoint and schedules defrost. Defrost is routine. The evaporator coil builds up ice and loses efficiency. So the system warms up a little for a few minutes to melt it all. It happens several times a day in any store.

The attackers used exactly that function. I won't speculate about the exact entry point. For what matters here, the big picture is enough. Someone got access to a dashboard that sends commands to many stores. Then they fired a legitimate command at the wrong time, at scale.

If prices were affected too, it makes sense to assume they lived in the same store management ecosystem. That could be the POS, the ERP or the electronic shelf labels. When everything goes through one place, one bad login takes everything down at once.

Why a defrost turns into a loss

A scheduled defrost is short. It's calculated so the product temperature stays in the safe range. A forced, extended or repeated defrost is a different story.

Think of an ice cream freezer. It needs to stay close to -18 °C. If it warms up and freezes again, the product may look fine, but the texture is gone and health rules say it goes in the bin. Meat, chicken and dairy follow the same logic. Too long above about 5 °C, and it's trash. Nobody risks poisoning a customer to save the stock.

Now multiply that by hundreds of stores. That's lost product, staff hours spent checking temperatures with a thermometer in hand, and empty shelves. Add late deliveries and wrong prices on the shelf. Nobody had to encrypt a single file to do real damage.

Somewhere out there, the ice cream turned into a milkshake at three in the morning without anyone ordering one.

IoT security: the legitimate command is the weapon

This is where the case gets interesting for developers. We tend to picture attacks as something weird. A malformed payload, SQL injection, a memory exploit. Here the command was perfectly valid. The system did exactly what it was told.

The most dangerous command in your system is the one it was built to obey.

That changes the question. Checking that the request is well formed isn't enough. You need to ask if this command makes sense, from this user, in this amount, at this time.

And there's a detail few people face. If the attacker owns the central dashboard, they have the central dashboard's credentials. Valid signature, valid token, valid session. All your authentication will say "go ahead". The defense left is to limit what legitimate access can do in one go.

How I'd protect remote commands with Bun

Let's assume a gateway in each store, running Bun. It receives commands from headquarters and forwards them to the controllers. It's not the only possible design, but it's common and works well as an example.

The first step is the basics done right. Signed commands, a closed list of allowed types and a short expiry so replays get rejected.

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("invalid signature", { status: 401 });
    }

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

    // forward to the controller...
    return Response.json({ ok: true });
  },
});

None of this is new. Bun runs node:crypto without any fuss, and timingSafeEqual keeps the signature from leaking through response timing. But like I said, this only stops people who don't have the key. Against a compromised dashboard, it does nothing.

How do you limit the damage of a bulk command?

The part that would really have made a difference is the brake. A simple counter that says: "more than X manual defrosts in Y minutes? Stop everything and call a human." With bun:sqlite, that fits in a few lines and leaves an audit trail too.

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; // calibrate from the store's history

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

Three details matter more than the code:

  • The limit lives in the store, not at headquarters. If it lives on the same server that got breached, the attacker turns it off before acting.
  • The number comes from history. Look at six months of logs and find out how many manual defrosts a store gets on a normal day. If the answer is "almost none", three in ten minutes is already a lot.
  • Denied attempts get logged too. A burst of blocks at three in the morning is your fire alarm.

And the last line of defense shouldn't even be server software. It should live in the controller itself. Defrost aborts on its own if the product temperature crosses a fixed limit, no matter who sent the command. That's firmware and physics, and no remote dashboard should be able to override it.

What changes in your workflow

You may never write software for a fridge. But almost every dev has built an endpoint that acts in bulk. Reprocessing orders, resending an email to the whole user base, updating the price of every product, deactivating users by filter. It's the same family of problem.

A few questions I started asking in code review:

  • If this credential leaks, what's the worst command it can fire?
  • Is there a cap on how many records, devices or stores a single call can touch?
  • Does a bulk operation require a second person to approve it?
  • Is the log of who sent what stored somewhere the system itself can't erase?

None of these answers needs a new framework. It's a table, a counter and an if. The hard part is remembering to add them before the incident, not in the post-mortem.

My take

A button that acts on a thousand stores at once, with no limit and no second confirmation, is a bug, not a feature. We learned how to protect data. We're still learning how to protect actions, especially when the action leaves the server and warms up a real freezer.

Bun won't save anyone on its own. What it does well is remove the excuse. HTTP server, SQLite and crypto come in the box, so adding a decent brake costs an afternoon. If you want to see how I apply this kind of care to real systems, take a look at the projects I maintain.

LinkedIn summary

Hackers broke into supermarkets in Europe and didn't steal a single piece of data. They just told the fridges to defrost.

The command was valid. The system obeyed, in hundreds of stores at once, and the ice cream turned into a milkshake at three in the morning.

The most dangerous command in your system is the one it was built to obey.

If the central dashboard falls, a valid signature and a valid token won't stop anyone. What saves you is a local brake: a cap on actions per time window, a log the system can't erase, and a physical lock on the controller.

With Bun and bun:sqlite, that brake fits in an afternoon. And it applies to any endpoint that acts in bulk, not just fridges.

I wrote up the full case with code on the blog. What's the worst command a leaked credential can fire in your system?

#CyberSecurity #IoT #Bun #TypeScript #Backend