The Car That Only Asked for Its VIN — JavaScript Bug Hunt

Modelled on the Nissan Leaf / NissanConnect EV disclosure, February 2016: security researcher Troy Hunt showed that the API behind Nissan's companion app…

  • Language: JavaScript
  • Layer: Backend
  • Difficulty: Medium
  • Concepts: Auth, Security
  • Modelled on: Nissan Leaf · 2016
  • Visible tests: the owner can read their trips; a request with only a VIN is refused
  • Reward: 50 XP for a complete fix

Briefing

Modelled on the Nissan Leaf / NissanConnect EV disclosure, February 2016: security researcher Troy Hunt showed that the API behind Nissan's companion app identified a car by its VIN alone. Anyone who knew or guessed a Leaf's VIN could switch its climate control on and off and read its recent trip history — no login, no link to the owner. Nissan took the service offline while it was fixed.

This reconstruction is that remote-control endpoint. It looks a car up by VIN and does what it is asked.

Fix handle so only the car's owner, with a live session, can reach it.

Bug report

BUG-LEAF-VIN · Priority: Critical · Reported by: external researcher

handle(req, store) with req = { vin, action, token, now } returns { status, body }. store.sessions: token -> { userId, expiresAt }; store.vehicles: vin -> { ownerId, climate, trips }.

Checks, in this order:

  1. Authentication: req.token must name a session in store.sessions whose expiresAt > req.now. Otherwise -> { status: 401, body: { error: "unauthenticated" } }.
  2. Authorisation: the vehicle must exist AND vehicle.ownerId must equal the session's userId. Otherwise -> { status: 403, body: { error: "forbidden" } } (the same answer for an unknown VIN, so VINs cannot be probed).
  3. Actions: "climate_on" / "climate_off" set vehicle.climate and return { status: 200, body: { vin, climate } }; "trips" returns { status: 200, body: { vin, trips } }; anything else -> { status: 400, body: { error: "unknown action" } }.

A refused request must not change any vehicle.

Observed: a request carrying only a VIN turns on a stranger's heater.

Logs

[carwings] POST /climate vin=SJNFAAZE0U60XXXXX token=<none> -> 200
[carwings] GET /trips vin=SJNFAAZE0U60XXXXX token=<none> -> 200 (14 trips)

The code as shipped

src/telematics/api.js (editable)

// Remote API for the companion app: climate control and trip history.
exports.handle = function (req, store) {
  var car = store.vehicles[req.vin];
  if (!car) return { status: 404, body: { error: "unknown vehicle" } };

  if (req.action === "climate_on" || req.action === "climate_off") {
    car.climate = req.action === "climate_on";
    return { status: 200, body: { vin: req.vin, climate: car.climate } };
  }
  if (req.action === "trips") {
    return { status: 200, body: { vin: req.vin, trips: car.trips } };
  }
  return { status: 400, body: { error: "unknown action" } };
};

Read-only context: src/telematics/fixtures.js.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.