Changing pickup location for someone else's parcel at Instabox
TL;DR
In December 2020, parcel IDs were a two-character sender prefix plus a counter, and the page for changing a pickup location treated the ID as the only credential. Anyone willing to count could move someone else’s parcel, and an unauthenticated API returned the recipient’s street address for the same ID. The critical parts were fixed in under a week; rate limiting and CORS were still open when the thread ended.
Instabox is the Swedish parcel locker brand that launched in Sweden in 2015. In December 2020 I was on the receiving end of a parcel with a tracking link that looked a little too easy to edit.
The report was sent in December 2020 and the fix landed the same month. I have not re-tested the service since, so nothing below should be read as a description of how Instabox works today.
Timeline
-
First contact with customer service, asking for a secure way to disclose a security issue. They reply and ask for details to forward internally.
-
Full report sent to Johan Lundin, with a 90-day publication date set to 10 March 2021. The CTO, Staffan Gabrielsson, picks it up the same evening.
-
Plan: require a postal code or phone number before showing anything on the tracking link. Deploy the next day.
-
Fix live. Asked me to re-test. No bounty program; merch offered instead.
-
Re-test: the most critical issues were fixed. Rate limiting and CORS were still open. Rate limiting confirmed as high priority; CORS lower priority.
-
Publication of this post, 90 days after the report.
Catching my attention
I had a parcel on the way and opened the tracking link. The URL contained a parcel ID that looked less like a random string and more like a sender prefix followed by a counter: NN001.
The NN turned out to identify the sender, one prefix per merchant such as NetOnNet, and the number was that sender’s running parcel count. So NN002, NN003 and the rest of that sender’s parcels are one for loop away. A counter is not a secret. It is a map of every parcel that sender has handed to the service, and it tells you how many there are and in what order they were created.
The prefix was only two characters, so the sender list was enumerable too. Walk AA through ZZ, keep the ones that return a parcel, and you have a directory of merchants. Then walk each sender’s counter.
I opened the change-locker page for my own parcel and started editing the URL. These things usually fall apart not in the cryptography, but in the assumption that nobody would bother editing the address bar.
If you don’t care about the walkthrough, skip to the vulnerabilities.
The tracking flow
A normal delivery looked roughly like this: a parcel got an ID, the recipient got a link, and the tracking page let them choose a pickup locker. The interesting part was the page for changing that choice.
The change page lived at:
https://track.instabox.io/se-sv/change/<parcel_id>
Opening it with a valid parcel ID let you change the delivery locker. There was no login, no SMS code, no signed link, and no step that confirmed you were the recipient. The parcel ID was the entire authorization model.
Whatever the page posted when you picked a new locker, the server had no way of knowing whether the request came from the recipient, the sender, or someone who had counted their way to the ID.
The tracking API behind the site was just as open:
GET /v1/unauthenticated/track?parcel_id=<parcel_id> HTTP/1.1
Host: tracking-api.instabox.se
And the response contained more than a status:
{
"service": {
"recipient": {
"city": "Example City",
"country_code": "SE",
"street": "Example Street 1",
"zip": "12345"
}
}
}
That endpoint was unauthenticated, and the IDs were enumerable. The recipient’s address was available for any parcel you could count to. Parcels dating back to October 2019 were exposed.
The tracking API also sent a wildcard CORS header:
Access-Control-Allow-Origin: *
See MDN’s CORS guide for what that does. Combined with guessable IDs, it made the whole thing scriptable from a page someone might already have open.
The vulnerabilities
There were two separate vulnerabilities, plus a data exposure that made both worse.
The first was that parcel IDs were sequential. That is not a vulnerability on its own. It becomes one the moment an ID is treated as proof that you are allowed to do something, and here it was the only proof required.
The second was that the change-locker action had no verification. Given a parcel ID, anyone could move the parcel to another locker, apparently even for parcels that had already been collected, and the change could be made more than once per parcel. Each change could generate a confirmation SMS to the recipient.
Put together, a script could walk the ID space and move a large number of parcels to different lockers. The two-character prefix narrowed the range down to a single sender, but the prefix space was small enough to enumerate too, so the same script could discover every sender and then walk each one’s range. At a completely unremarkable ten requests per second, that is tens of thousands of attempts per hour.
The result would not be stolen goods so much as a very effective denial-of-service attack on the delivery experience: a flood of confirmation SMS, a lot of parcels sent to the wrong locker, and a support queue full of people asking what happened. For a service whose entire promise is that pickup should be simple, that is a bad place to be.
The exposed address made the whole thing worse. It is also what made the obvious fix insufficient on its own. Requiring a postal code before showing the tracking page only works if the postal code is not already in the API response. Gating a page with a value that the same API hands out for free is theatre.
Reporting the vulnerabilities
There was no security.txt and no vulnerability disclosure policy that I could find, so I started where a customer would start: the general customer service address, on 9 December 2020. An agent replied within the hour and asked for the report to forward internally. Johan Lundin passed it on, and the CTO picked it up the next day. It worked, but that was a helpful support agent, not a documented process.
The report described the ID enumeration, the unauthenticated change endpoint, the exposed recipient data, and what I thought it added up to. The core of it:
Your parcels turn out to have identifiers in the form of serial numbers. That is, by starting from “NN001”, you can find more parcels in the same series, for example “NN002”, “NN003” and so on.
Visiting https://track.instabox.io/se-sv/change/<parcel_id> with a given serial number lets the desired pickup locker be changed. I hope you understand the risk of allowing this action without any form of verification or signing. For example, it is possible to carry out a so-called brute-force attack. If a person intends to cause damage, they can have the desired locker changed for a significant number of parcels, apparently even for those already collected. The change can also be made more than once per parcel.
I also wrote that the exposed addresses needed to be assessed as a personal data incident under GDPR, and that they should check their access logs either way. Then I set a publication date: 90 days later, 10 March 2021.
A deadline is not a threat. It is how you make sure the report does not quietly disappear into a ticket queue, and how you give the person who found the issue a way to close the loop.
My recommendations
These were sent with the report. Several only work in combination.
- Remove all personal data from the public API, across every resource.
- Require the recipient’s phone number before confirming a locker change.
- Rate limit lookups per IP address.
- Rate limit changes per parcel within a reasonable time window.
- Mask or randomize active parcel IDs so they are harder to enumerate.
- Hide parcels that were collected long ago.
- Replace the wildcard CORS policy with an allowlist of legitimate origins.
- Review how security reports reach the right people.
Changes made since the report
The next day the CTO described the plan: require a postal code or phone number before showing anything on the tracking link, and deploy a day later.
I replied that the postal code gate only made sense if they also removed the recipient address from the public API. Otherwise you could guess nearby postal codes relative to the chosen locker and find your way back in. I asked for rate limiting in the same breath.
On 16 December the changes were live and they asked me to take another look. When I re-tested the next day, the critical issues were fixed: the tracking page required proof that you knew something about the parcel, and the address exposure was removed. You could no longer enumerate a parcel and then read the value you needed to unlock it.
Two things were still open: rate limiting and CORS.
The missing rate limit mattered most. If the phone sign-in endpoint accepts unlimited attempts, the new gate is only a speed bump, and the change endpoint is back in reach. I asked for permission to test it against one of my own parcels. The reply explained the tradeoff, but did not grant it, so I did not run the test: rate limiting was high priority but needed shared infrastructure all teams could use, while CORS was lower priority and already on the responsible team’s backlog. I agreed that rate limiting mattered more.
The thread ends before I got confirmation that the rate limit shipped. I did not keep probing; at that point the responsible thing was to let them finish the work.
They also did not have a bounty program at the time, which they were upfront about. They offered merch instead, and asked whether it could be sent to a locker at Stor Coop Tumba because home delivery had not reached Tullinge yet. I said US Medium or EU Large and called it a day. It remains the most Swedish exchange I have had about software security.
Conclusions
The specifics are from 2020 and the service has changed since. The design lessons have not.
Sequential IDs are an authorization problem waiting to happen. Use unguessable identifiers, or sign the links that contain them. A counter is an invitation.
A tracking link is a bearer token. If anyone with the link can act on the parcel, the link is the credential. Make it unguessable, scope it to one action, expire it, and do not put personal data behind it.
Proof of possession only works if the thing you are proving is not public. Fix the data exposure and the gate together, or the gate is decoration.
Rate limit the endpoints that mutate state or send SMS. Per IP is a start; per parcel matters too, or one attacker with many addresses can still cycle a single parcel.
CORS is not a security boundary. Fix it, but do not pretend it is the lock on the door.
Make the reporting path obvious. I started at info@ and got forwarded to the CTO within a day, which is better than most companies manage. That was a helpful support agent, not a documented process. A security.txt, a security page, or an address that reaches an engineer costs almost nothing and shortens the most important part of an incident: the beginning.
Credit
Thanks to Staffan Gabrielsson and Johan Lundin at Instabox for taking a cold email from a stranger seriously, shipping the important part in under a week, and being honest about what they had not done yet. The t-shirt was a nice touch; the Tech AW was cancelled for reasons everyone remembers.