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:
- Authentication: req.token must name a session in store.sessions whose expiresAt > req.now. Otherwise -> { status: 401, body: { error: "unauthenticated" } }.
- 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).
- 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.