I was traveling home and had a 21-hour layover at Delhi. I booked a hostel near the airport to get some much-needed rest. The hostel is part of a large chain serving more than 70 locations in India. The entire process at the hostel was digital, from booking to controlling the room. It looked super-impressive on the surface!
The room controls were hosted on a webpage and I wanted to turn the lights off โ that’s when I realized the controls weren’t loading. At first, I just wanted to sleep and catch my next flight, but the engineer in me wanted to turn the lights off come what may!

So I quickly opened the webpage on my laptop and realized that the controls are rendered inside an <iframe>. The iframe was formed using three things:
- A smartLink โ a base URL unique to each room
- A check-in date
- A check-out date
The final URL looked something like this:
<smartlink>?checkin=<iso-8601-date>&checkout=<iso-8601-date>
The app wasn’t loading because none of these three values were being set correctly in my session. All three values were supposed to be pulled from sessionStorage โ in my case, they simply weren’t there. So the iframe rendered blank.
Fine. But now I was curious: where does the smartLink come from, and how does the app build this URL?
Step 1: Reading the App’s Source Code โ Literally
The hostel’s web app was built on Next.js. I opened DevTools, went to the Sources tab, and started looking at the JavaScript chunks being served to the browser.
Next.js bundles and serves your application code to every visitor. In production, these chunks are usually minified into unreadable gibberish. But this app was shipping them largely readable โ and inside _app-77a77f911174a00e.js, I found the actual GraphQL queries the app uses to talk to its backend, sitting in plain text:
query getHostelRoomConfig(
$hostelId: ID!,
$roomNo: String!
) {
getHostelRoomConfig(
hostelId: $hostelId,
roomNo: $roomNo
) {
_id
smartLink
status
smartLockId
masterOtp
}
}
There it was. The app fetches the smartLink by calling getHostelRoomConfig with a hostelId and a roomNo. I just needed both of those.
Several other queries were also exposed โ getBookingByUniqueReservationId, getBookingDetailsByReservationNo, getAllOrders โ essentially the entire data model of the platform, readable by anyone who opened DevTools.
Step 2: Getting the hostelId โ It Was Predictable
I knew my reservation number and the pattern was fairly easy to decipher. The naming convention alone was telling โ REServation-Hostel-Delhi-International-Airport-<xxxx>. Sequential IDs. But I didn’t even need to guess โ I just called the query I’d found in the bundle:
query getBookingDetailsByReservationNo(
$reservationNo: String!
) {
getBookingDetailsByReservationNo(
reservationNo: $reservationNo
) {
ReservationNo
FirstName
LastName
hostelName
hostelId
...
}
}
{
"hostelId": "random-string"
}
One query. No authentication beyond knowing a reservation number. I now had the hostel’s internal ID.
Step 3: BOLA โ Every Room, No Questions Asked
Now I had hostelId. The only other input getHostelRoomConfig needed was a roomNo. I tried my own room first. Worked perfectly.
Then I tried my neighbor’s room number.
{
"data": {
"getHostelRoomConfig": {
"_id": "693fbdd0e876bxxxxxxxxx",
"smartLink": "https://app.xyz.io/sdk/<roomId>/<smartLockId>",
"status": true,
"smartLockId": "6936935c191xxxxxxxxx",
"masterOtp": "965xxx"
}
}
}
Full config. smartLink. And a field called masterOtp.
The backend performed zero checks on whether the person calling this query had any connection to Room 101. No session validation. No ownership check. Just: you asked, here you go.
This is Broken Object Level Authorization (BOLA) โ the #1 vulnerability on the OWASP API Security Top 10. An attacker can simply loop through room numbers โ 101, 102, 103… โ and harvest the smartLink and masterOtp for every room in the building.
Now about that masterOtp. This is the physical override code for the door lock. The code you use when everything else fails. It should never leave a secure backend system โ and it certainly has no business being returned by an API endpoint whose entire job is to load a webpage that controls the lights. This is a textbook violation of the Principle of Least Privilege.
Step 4: The Client Decides When Your Stay Is Valid
I now had the smartLink for Room 101. But to activate the room controls, the SDK also needs check-in and check-out dates in the URL. I fetched those from another exposed query:
{
"ArrivalDate": "2026-03-11T00:00:00.000Z",
"DepartureDate": "2026-03-12T00:00:00.000Z"
}
Putting it all together, the final URL to take full control of Room 101 was:
https://app.xyz.io/sdk/69368f0eexxxxxx/6936935c19169xxxxxx?checkin=2026-03-11&checkout=2026-03-12
Lights. AC. All controllable from a browser tab.
Here’s the second critical flaw: the SDK validates the session entirely based on those URL parameters. The dates come from sessionStorage on the client โ the browser decides when the stay is active. Which means you can pass any dates you want. A stay that expired yesterday? Just change `checkout` to next year. The server never verifies it. The client is trusted completely.

Never trust the client. This is day-one web security. The hotel thought they were being modern by adding a digital layer โ but they handed the keys to the browser.
The Full Kill Chain
- RECON โ Read GraphQL queries from the exposed Next.js JS bundle
- ENUMERATE โ Call
getBookingDetailsByReservationNoto get thehostelId - EXPLOIT โ Call
getHostelRoomConfigwithhostelId+ anyroomNo(no auth check) โ ReceivesmartLink+masterOtpfor any room in the building - BYPASS โ Craft the
smartLinkURL with arbitrary dates to pass the SDK’s client-side session check - CONTROL โ Full IoT access to lights, AC, other appliances + physical door override code
One bored engineer. One overnight layover. Every room in the building.
To be clear about the scale โ this isn’t a mom-and-pop guesthouse. It is one of India’s largest hostel chains, with 70+ properties across dozens of cities in India. The same API, the same SDK, the same vulnerability โ across all of them.
Beyond Insecure IoT
The IoT access was the most dramatic finding, but it wasn’t the only one. The same unauthenticated GraphQL API was leaking far more.
The getBookingByUniqueReservationId query returns full guest details โ name, room number, number of guests, check-in data โ for any reservation number you throw at it. Given that reservation numbers follow a predictable sequential pattern, enumerating guest data across the entire chain is trivial. Name, room number, stay dates โ all exposed, no authentication required.
Food and service orders were similarly accessible via getAllOrders, meaning a guest’s spending habits and in-room behavior were queryable by anyone.
But the most concerning discovery was beyond reads entirely. The exposed JS bundle also contained mutation operations โ meaning the API surface didn’t just allow data to be read, but potentially modified. I did not execute any mutations, but their exposure on an unauthenticated endpoint is deeply concerning. At a chain operating across dozens of properties, the blast radius of a malicious actor with write access to this API is hard to overstate.
This stopped being a “smart lock” bug the moment I saw customer PII flowing freely out of an API that anyone with DevTools and five minutes could find.
Responsible Disclosure
I reported these findings to the hostel before publishing. The critical issues are the BOLA on getHostelRoomConfig and the masterOtp being exposed to a web client โ the client-side date bypass is a secondary concern if the API is properly locked down upstream.
If you’re building on top of IoT SDKs or third-party room control systems: enforce object-level authorization on every API call, validate sessions server-side, and never expose physical security credentials to the browser. The digital layer is only as strong as its weakest assumption โ and assuming the browser is honest is a very weak assumption.
Leave a Reply